openapi: 3.1.0
info:
  title: Kanbalone API
  version: 0.9.34
  summary: Ultra-light local personal kanban API for human and AI collaboration
  license:
    name: MIT
    identifier: MIT
servers:
  - url: http://127.0.0.1:3000
security: []
paths:
  /api/health:
    get:
      summary: Health check
      operationId: getHealth
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:
                    type: boolean
                required: [ok]
        "400":
          $ref: "#/components/responses/Error"
        "500":
          $ref: "#/components/responses/Error"

  /api/meta:
    get:
      summary: Get app metadata
      operationId: getMeta
      responses:
        "200":
          description: App metadata
          content:
            application/json:
              schema:
                type: object
                properties:
                  name:
                    type: string
                  version:
                    type: string
                  remoteProviders:
                    type: array
                    items:
                      type: object
                      properties:
                        id:
                          type: string
                        hasCredential:
                          type: boolean
                      required: [id, hasCredential]
                required: [name, version, remoteProviders]
        "400":
          $ref: "#/components/responses/Error"
        "500":
          $ref: "#/components/responses/Error"

  /api/remote-diagnostics:
    get:
      summary: List remote provider diagnostic status
      operationId: listRemoteProviderDiagnostics
      responses:
        "200":
          description: Remote provider credential configuration status
          content:
            application/json:
              schema:
                type: object
                properties:
                  providers:
                    type: array
                    items:
                      type: object
                      properties:
                        id:
                          type: string
                        hasCredential:
                          type: boolean
                        status:
                          type: string
                          enum: [configured, missing_credential]
                      required: [id, hasCredential, status]
                required: [providers]
        "400":
          $ref: "#/components/responses/Error"
        "500":
          $ref: "#/components/responses/Error"
    post:
      summary: Check a remote issue credential
      description: Attempts to resolve a remote issue with an exact-scope configured provider credential and returns a token-safe diagnostic result. Wildcard credentials are not used for user-supplied diagnostic URLs.
      operationId: checkRemoteCredential
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/RemoteDiagnosticRequest"
      responses:
        "200":
          description: Remote issue is reachable with the configured credential
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/RemoteDiagnosticResult"
        "400":
          description: Diagnostic failed or the provider is unsupported/missing credentials
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/RemoteDiagnosticResult"
        "500":
          $ref: "#/components/responses/Error"

  /api/boards:
    get:
      summary: List boards
      operationId: listBoards
      responses:
        "200":
          description: Board list
          content:
            application/json:
              schema:
                type: object
                properties:
                  boards:
                    type: array
                    items:
                      $ref: "#/components/schemas/Board"
                required: [boards]
        "400":
          $ref: "#/components/responses/Error"
        "500":
          $ref: "#/components/responses/Error"
    post:
      summary: Create board
      operationId: createBoard
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                name:
                  type: string
                laneNames:
                  type: array
                  items:
                    type: string
              required: [name]
      responses:
        "201":
          description: Created board detail
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/BoardDetail"
        "400":
          $ref: "#/components/responses/Error"

  /api/boards/import:
    post:
      summary: Import a board
      description: Supports local board import payloads, including larger seed datasets used for bulk local setup and performance testing.
      operationId: importBoard
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/BoardExport"
      responses:
        "201":
          description: Imported board detail
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/BoardDetail"
        "400":
          $ref: "#/components/responses/Error"

  /api/boards/{boardId}:
    get:
      summary: Get board shell
      operationId: getBoard
      parameters:
        - $ref: "#/components/parameters/BoardId"
      responses:
        "200":
          description: Board shell
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/BoardShell"
        "404":
          $ref: "#/components/responses/Error"
    patch:
      summary: Rename board
      operationId: updateBoard
      parameters:
        - $ref: "#/components/parameters/BoardId"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                name:
                  type: string
              required: [name]
      responses:
        "200":
          description: Updated board
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Board"
        "400":
          $ref: "#/components/responses/Error"
        "404":
          $ref: "#/components/responses/Error"
    delete:
      summary: Delete board and related data
      operationId: deleteBoard
      parameters:
        - $ref: "#/components/parameters/BoardId"
      responses:
        "204":
          description: Deleted
        "400":
          $ref: "#/components/responses/Error"
        "404":
          $ref: "#/components/responses/Error"

  /api/boards/{boardId}/events:
    get:
      summary: Subscribe to board updates via SSE
      operationId: subscribeBoardEvents
      parameters:
        - $ref: "#/components/parameters/BoardId"
      responses:
        "200":
          description: SSE stream
          content:
            text/event-stream:
              schema:
                type: string
                example: |
                  data: {"boardId":3,"event":"board_updated","sentAt":"2026-04-10T00:00:00.000Z"}
        "404":
          $ref: "#/components/responses/Error"

  /api/boards/{boardId}/lanes:
    get:
      summary: List lanes
      operationId: listLanes
      parameters:
        - $ref: "#/components/parameters/BoardId"
      responses:
        "200":
          description: Lane list
          content:
            application/json:
              schema:
                type: object
                properties:
                  lanes:
                    type: array
                    items:
                      $ref: "#/components/schemas/Lane"
                required: [lanes]
        "404":
          $ref: "#/components/responses/Error"
    post:
      summary: Create lane
      operationId: createLane
      parameters:
        - $ref: "#/components/parameters/BoardId"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                name:
                  type: string
              required: [name]
      responses:
        "201":
          description: Created lane
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Lane"
        "400":
          $ref: "#/components/responses/Error"
        "404":
          $ref: "#/components/responses/Error"

  /api/boards/{boardId}/lanes/reorder:
    post:
      summary: Reorder lanes
      operationId: reorderLanes
      parameters:
        - $ref: "#/components/parameters/BoardId"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                laneIds:
                  type: array
                  items:
                    type: integer
              required: [laneIds]
      responses:
        "200":
          description: Reordered lanes
          content:
            application/json:
              schema:
                type: object
                properties:
                  lanes:
                    type: array
                    items:
                      $ref: "#/components/schemas/Lane"
                required: [lanes]
        "400":
          $ref: "#/components/responses/Error"

  /api/lanes/{laneId}:
    patch:
      summary: Rename lane
      operationId: updateLane
      parameters:
        - $ref: "#/components/parameters/LaneId"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                name:
                  type: string
              required: [name]
      responses:
        "200":
          description: Updated lane
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Lane"
        "400":
          $ref: "#/components/responses/Error"
        "404":
          $ref: "#/components/responses/Error"
    delete:
      summary: Delete empty lane
      operationId: deleteLane
      parameters:
        - $ref: "#/components/parameters/LaneId"
      responses:
        "204":
          description: Deleted
        "404":
          $ref: "#/components/responses/Error"
        "409":
          $ref: "#/components/responses/Error"

  /api/boards/{boardId}/tags:
    get:
      summary: List tags
      operationId: listTags
      parameters:
        - $ref: "#/components/parameters/BoardId"
      responses:
        "200":
          description: Tag list
          content:
            application/json:
              schema:
                type: object
                properties:
                  tags:
                    type: array
                    items:
                      $ref: "#/components/schemas/Tag"
                required: [tags]
        "404":
          $ref: "#/components/responses/Error"
    post:
      summary: Create tag
      operationId: createTag
      parameters:
        - $ref: "#/components/parameters/BoardId"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                name:
                  type: string
                color:
                  type: string
              required: [name]
      responses:
        "201":
          description: Created tag
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Tag"
        "400":
          $ref: "#/components/responses/Error"
        "404":
          $ref: "#/components/responses/Error"
        "409":
          $ref: "#/components/responses/Error"

  /api/tags/{tagId}:
    patch:
      summary: Update tag
      operationId: updateTag
      parameters:
        - $ref: "#/components/parameters/TagId"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                name:
                  type: string
                color:
                  type: string
      responses:
        "200":
          description: Updated tag
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Tag"
        "404":
          $ref: "#/components/responses/Error"
    delete:
      summary: Delete tag
      operationId: deleteTag
      parameters:
        - $ref: "#/components/parameters/TagId"
      responses:
        "204":
          description: Deleted
        "404":
          $ref: "#/components/responses/Error"

  /api/boards/{boardId}/tickets:
    get:
      summary: List ticket summaries
      description: Lightweight summary endpoint for board rendering, filtering, search, and automation on large boards. Fetch individual ticket detail separately when body, comments, or expanded relations are needed.
      operationId: listTickets
      parameters:
        - $ref: "#/components/parameters/BoardId"
        - name: lane_id
          in: query
          schema:
            type: integer
        - name: tag
          in: query
          schema:
            type: string
        - name: resolved
          in: query
          schema:
            type: boolean
        - name: completed
          in: query
          deprecated: true
          description: Legacy alias for resolved.
          schema:
            type: boolean
        - name: archived
          in: query
          schema:
            type: string
            enum: ["true", "false", "all"]
        - name: q
          in: query
          description: Search title, body Markdown, local ticket refs such as #10, tracked remote refs, external references, provider refs such as gh#10, external-only refs such as ext#10, or tracked-remote-only refs such as remote#10.
          schema:
            type: string
      responses:
        "200":
          description: Ticket summary list
          content:
            application/json:
              schema:
                type: object
                properties:
                  tickets:
                    type: array
                    items:
                      $ref: "#/components/schemas/TicketSummary"
                required: [tickets]
        "404":
          $ref: "#/components/responses/Error"
    post:
      summary: Create ticket
      operationId: createTicket
      parameters:
        - $ref: "#/components/parameters/BoardId"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              allOf:
                - $ref: "#/components/schemas/TicketUpsert"
                - type: object
                  required: [laneId, title]
      responses:
        "201":
          description: Created ticket
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Ticket"
        "400":
          $ref: "#/components/responses/Error"
        "404":
          $ref: "#/components/responses/Error"

  /api/boards/{boardId}/remote-import:
    post:
      summary: Import a remote issue into a board
      description: Creates one local ticket from a remote issue snapshot. The created ticket uses the remote title and copies the remote body into the local body for initial implementation context. Remote metadata is tracked on the ticket, but board export omits it.
      operationId: importRemoteIssue
      parameters:
        - $ref: "#/components/parameters/BoardId"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/RemoteIssueImport"
      responses:
        "201":
          description: Imported tracked ticket
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Ticket"
        "400":
          $ref: "#/components/responses/Error"
        "404":
          $ref: "#/components/responses/Error"
        "409":
          $ref: "#/components/responses/Error"

  /api/boards/{boardId}/remote-import/preview:
    post:
      summary: Preview a remote issue before import
      description: Resolves a remote issue without creating a local ticket and reports whether the remote issue has already been imported.
      operationId: previewRemoteIssueImport
      parameters:
        - $ref: "#/components/parameters/BoardId"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/RemoteIssueImport"
      responses:
        "200":
          description: Remote issue preview
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/RemoteIssueImportPreview"
        "400":
          $ref: "#/components/responses/Error"
        "404":
          $ref: "#/components/responses/Error"

  /api/boards/{boardId}/tickets/bulk-complete:
    post:
      summary: Bulk update resolved state
      operationId: bulkResolveTickets
      parameters:
        - $ref: "#/components/parameters/BoardId"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                ticketIds:
                  type: array
                  items:
                    type: integer
                isResolved:
                  type: boolean
                  description: Canonical resolved state. Use this instead of isCompleted.
                isCompleted:
                  type: boolean
                  deprecated: true
                  description: Legacy alias for isResolved. Use isResolved for new clients.
              required: [ticketIds]
      responses:
        "200":
          description: Updated ticket summaries
          content:
            application/json:
              schema:
                type: object
                properties:
                  tickets:
                    type: array
                    items:
                      $ref: "#/components/schemas/TicketSummary"
                required: [tickets]
        "400":
          $ref: "#/components/responses/Error"
        "404":
          $ref: "#/components/responses/Error"

  /api/boards/{boardId}/tickets/bulk-transition:
    post:
      summary: Bulk transition tickets by lane name
      operationId: bulkTransitionTickets
      parameters:
        - $ref: "#/components/parameters/BoardId"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                ticketIds:
                  type: array
                  items:
                    type: integer
                laneName:
                  type: string
                isResolved:
                  type: boolean
                  description: Canonical resolved state. Use this instead of isCompleted.
                isCompleted:
                  type: boolean
                  deprecated: true
                  description: Legacy alias for isResolved. Use isResolved for new clients.
              required: [ticketIds, laneName]
      responses:
        "200":
          description: Updated ticket summaries
          content:
            application/json:
              schema:
                type: object
                properties:
                  tickets:
                    type: array
                    items:
                      $ref: "#/components/schemas/TicketSummary"
                required: [tickets]
        "400":
          $ref: "#/components/responses/Error"
        "404":
          $ref: "#/components/responses/Error"

  /api/boards/{boardId}/tickets/bulk-move:
    post:
      summary: Bulk move tickets to another board and lane
      description: Moves all selected tickets in one transaction. If any selected ticket does not belong to the source board or the target board/lane is invalid, no tickets are moved.
      operationId: bulkMoveTickets
      parameters:
        - $ref: "#/components/parameters/BoardId"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                ticketIds:
                  type: array
                  items:
                    type: integer
                boardId:
                  type: integer
                laneId:
                  type: integer
              required: [ticketIds, boardId, laneId]
      responses:
        "200":
          description: Moved ticket summaries from the destination board
          content:
            application/json:
              schema:
                type: object
                properties:
                  tickets:
                    type: array
                    items:
                      $ref: "#/components/schemas/TicketSummary"
                required: [tickets]
        "400":
          $ref: "#/components/responses/Error"
        "404":
          $ref: "#/components/responses/Error"

  /api/boards/{boardId}/tickets/reorder:
    post:
      summary: Reorder tickets and move between lanes
      description: Legacy full-board reorder endpoint. The request must include every non-archived ticket returned by the board detail for the board. UI clients should prefer `/api/tickets/{ticketId}/position` for single-ticket drag-and-drop moves.
      deprecated: true
      operationId: reorderTickets
      parameters:
        - $ref: "#/components/parameters/BoardId"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                items:
                  type: array
                  items:
                    type: object
                    properties:
                      ticketId:
                        type: integer
                      laneId:
                        type: integer
                      position:
                        type: integer
                    required: [ticketId, laneId, position]
              required: [items]
      responses:
        "200":
          description: Reordered tickets
          content:
            application/json:
              schema:
                type: object
                properties:
                  tickets:
                    type: array
                    items:
                      $ref: "#/components/schemas/Ticket"
                required: [tickets]
        "400":
          $ref: "#/components/responses/Error"

  /api/boards/{boardId}/export:
    get:
      summary: Export board
      operationId: exportBoard
      parameters:
        - $ref: "#/components/parameters/BoardId"
      responses:
        "200":
          description: Export payload
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/BoardExport"
        "404":
          $ref: "#/components/responses/Error"

  /api/tickets/{ticketId}:
    get:
      summary: Get ticket detail
      operationId: getTicket
      parameters:
        - $ref: "#/components/parameters/TicketId"
      responses:
        "200":
          description: Ticket detail
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Ticket"
        "404":
          $ref: "#/components/responses/Error"
    patch:
      summary: Update ticket
      operationId: updateTicket
      parameters:
        - $ref: "#/components/parameters/TicketId"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/TicketUpsert"
      responses:
        "200":
          description: Updated ticket
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Ticket"
        "400":
          $ref: "#/components/responses/Error"
        "404":
          $ref: "#/components/responses/Error"
    delete:
      summary: Delete ticket
      operationId: deleteTicket
      parameters:
        - $ref: "#/components/parameters/TicketId"
      responses:
        "204":
          description: Deleted
        "404":
          $ref: "#/components/responses/Error"

  /api/tickets/{ticketId}/transition:
    patch:
      summary: Transition ticket by lane name
      operationId: transitionTicket
      parameters:
        - $ref: "#/components/parameters/TicketId"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                laneName: { type: string }
                isResolved:
                  type: boolean
                  description: Canonical resolved state. Use this instead of isCompleted.
                isCompleted:
                  type: boolean
                  deprecated: true
                  description: Legacy alias for isResolved. Use isResolved for new clients.
              required: [laneName]
      responses:
        "200":
          description: Transitioned ticket
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Ticket"
        "400":
          $ref: "#/components/responses/Error"
        "404":
          $ref: "#/components/responses/Error"

  /api/tickets/{ticketId}/position:
    patch:
      summary: Move ticket within its board by lane and position
      description: Moves one ticket to a lane on the same board and inserts it near the requested neighboring tickets, or at the requested numeric position when no anchors are provided. Other tickets in the affected lanes are reindexed server-side.
      operationId: positionTicket
      parameters:
        - $ref: "#/components/parameters/TicketId"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                laneId:
                  type: integer
                position:
                  type: integer
                  minimum: 0
                beforeTicketId:
                  anyOf:
                    - type: integer
                    - type: "null"
                afterTicketId:
                  anyOf:
                    - type: integer
                    - type: "null"
              required: [laneId]
      responses:
        "200":
          description: Repositioned ticket
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Ticket"
        "400":
          $ref: "#/components/responses/Error"
        "404":
          $ref: "#/components/responses/Error"

  /api/tickets/{ticketId}/remote-refresh:
    post:
      summary: Refresh remote issue snapshot
      description: Re-fetches remote title/body/state for a tracked ticket. The local bodyMarkdown is not overwritten.
      operationId: refreshRemoteIssue
      parameters:
        - $ref: "#/components/parameters/TicketId"
      responses:
        "200":
          description: Refreshed tracked ticket
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Ticket"
        "400":
          $ref: "#/components/responses/Error"
        "404":
          $ref: "#/components/responses/Error"

  /api/tickets/{ticketId}/external-references/{kind}:
    put:
      summary: Set external ticket reference
      description: Idempotently sets a non-tracking external reference for the ticket. External references do not participate in the one-import-per-remote-issue constraint and do not expose refresh, sync, or push actions.
      operationId: setTicketExternalReference
      parameters:
        - $ref: "#/components/parameters/TicketId"
        - $ref: "#/components/parameters/ExternalReferenceKind"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/TicketExternalReferenceSet"
      responses:
        "200":
          description: Ticket with updated external references
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Ticket"
        "400":
          $ref: "#/components/responses/Error"
        "404":
          $ref: "#/components/responses/Error"
    delete:
      summary: Remove external ticket reference
      operationId: removeTicketExternalReference
      parameters:
        - $ref: "#/components/parameters/TicketId"
        - $ref: "#/components/parameters/ExternalReferenceKind"
      responses:
        "200":
          description: Ticket with updated external references
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Ticket"
        "400":
          $ref: "#/components/responses/Error"
        "404":
          $ref: "#/components/responses/Error"

  /api/tickets/{ticketId}/move:
    post:
      summary: Move ticket to another board and lane
      description: Moves one ticket to the target board and lane. When the target board differs from the source board, matching tag names are preserved and parent, child, and blocker links are cleared so relations do not cross boards.
      operationId: moveTicket
      parameters:
        - $ref: "#/components/parameters/TicketId"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                boardId:
                  type: integer
                laneId:
                  type: integer
              required: [boardId, laneId]
      responses:
        "200":
          description: Moved ticket
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Ticket"
        "400":
          $ref: "#/components/responses/Error"
        "404":
          $ref: "#/components/responses/Error"


  /api/tickets/{ticketId}/relations:
    get:
      summary: Get ticket relations
      operationId: getTicketRelations
      parameters:
        - $ref: "#/components/parameters/TicketId"
      responses:
        "200":
          description: Ticket relations
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/TicketRelations"
        "404":
          $ref: "#/components/responses/Error"

  /api/tickets/{ticketId}/comments:
    get:
      summary: List comments
      description: Returns comments with the newest comment first.
      operationId: listComments
      parameters:
        - $ref: "#/components/parameters/TicketId"
      responses:
        "200":
          description: Comment list
          content:
            application/json:
              schema:
                type: object
                properties:
                  comments:
                    type: array
                    items:
                      $ref: "#/components/schemas/Comment"
                required: [comments]
        "404":
          $ref: "#/components/responses/Error"
    post:
      summary: Add comment
      operationId: addComment
      parameters:
        - $ref: "#/components/parameters/TicketId"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                bodyMarkdown:
                  type: string
              required: [bodyMarkdown]
      responses:
        "201":
          description: Created comment
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Comment"
        "400":
          $ref: "#/components/responses/Error"
        "404":
          $ref: "#/components/responses/Error"

  /api/comments/{commentId}:
    patch:
      summary: Update comment
      operationId: updateComment
      parameters:
        - name: commentId
          in: path
          required: true
          schema:
            type: integer
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                bodyMarkdown:
                  type: string
              required: [bodyMarkdown]
      responses:
        "200":
          description: Updated comment
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Comment"
        "400":
          $ref: "#/components/responses/Error"
        "404":
          $ref: "#/components/responses/Error"
    delete:
      summary: Delete comment
      operationId: deleteComment
      parameters:
        - name: commentId
          in: path
          required: true
          schema:
            type: integer
      responses:
        "204":
          description: Deleted
        "404":
          $ref: "#/components/responses/Error"

  /api/comments/{commentId}/push-remote:
    post:
      summary: Push a local comment to the tracked remote issue
      description: Posts the local comment body to the linked remote issue and marks the comment as pushed. Already-pushed and in-progress comments are rejected to avoid duplicate remote comments.
      operationId: pushCommentToRemote
      parameters:
        - name: commentId
          in: path
          required: true
          schema:
            type: integer
      responses:
        "200":
          description: Comment with updated remote sync state
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Comment"
        "400":
          $ref: "#/components/responses/Error"
        "404":
          $ref: "#/components/responses/Error"
        "409":
          $ref: "#/components/responses/Error"

  /api/tickets/{ticketId}/activity:
    get:
      summary: List ticket activity
      description: Returns activity entries with the newest entry first.
      operationId: listTicketActivity
      parameters:
        - $ref: "#/components/parameters/TicketId"
      responses:
        "200":
          description: Activity list
          content:
            application/json:
              schema:
                type: object
                properties:
                  activity:
                    type: array
                    items:
                      $ref: "#/components/schemas/ActivityLog"
                required: [activity]
        "404":
          $ref: "#/components/responses/Error"

  /api/tickets/{ticketId}/events:
    get:
      summary: List structured ticket events
      description: Returns machine-readable ticket events with the newest event first.
      operationId: listTicketEvents
      parameters:
        - $ref: "#/components/parameters/TicketId"
      responses:
        "200":
          description: Event list
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/TicketEventsResponse"
        "404":
          $ref: "#/components/responses/Error"
    post:
      summary: Add structured ticket event
      description: Stores a source-specific event for AI and automation workflows without changing the regular activity log shape.
      operationId: addTicketEvent
      parameters:
        - $ref: "#/components/parameters/TicketId"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/TicketEventCreateBody"
      responses:
        "201":
          description: Created event
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/TicketEvent"
        "400":
          $ref: "#/components/responses/Error"
        "404":
          $ref: "#/components/responses/Error"

  /api/tickets/{ticketId}/tag-reasons:
    get:
      summary: List ticket tag reasons
      description: Returns the ticket's currently attached tags with optional reason metadata.
      operationId: listTicketTagReasons
      parameters:
        - $ref: "#/components/parameters/TicketId"
      responses:
        "200":
          description: Tag reason list
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/TicketTagReasonsResponse"
        "404":
          $ref: "#/components/responses/Error"

  /api/tickets/{ticketId}/tags/{tagId}:
    post:
      summary: Attach tag with optional reason
      description: Attaches an existing board tag to the ticket and upserts reason metadata for that tag.
      operationId: setTicketTagReason
      parameters:
        - $ref: "#/components/parameters/TicketId"
        - $ref: "#/components/parameters/TagId"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/TicketTagReasonSetBody"
      responses:
        "200":
          description: Attached tag reason
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/TicketTagReason"
        "400":
          $ref: "#/components/responses/Error"
        "404":
          $ref: "#/components/responses/Error"
    delete:
      summary: Remove tag from ticket
      operationId: removeTicketTag
      parameters:
        - $ref: "#/components/parameters/TicketId"
        - $ref: "#/components/parameters/TagId"
      responses:
        "204":
          description: Removed
        "404":
          $ref: "#/components/responses/Error"

components:
  parameters:
    BoardId:
      name: boardId
      in: path
      required: true
      schema:
        type: integer
    LaneId:
      name: laneId
      in: path
      required: true
      schema:
        type: integer
    TagId:
      name: tagId
      in: path
      required: true
      schema:
        type: integer
    TicketId:
      name: ticketId
      in: path
      required: true
      schema:
        type: integer
    ExternalReferenceKind:
      name: kind
      in: path
      required: true
      schema:
        type: string
        pattern: "^[A-Za-z][A-Za-z0-9_-]{0,63}$"
  responses:
    Error:
      description: Error response
      content:
        application/json:
          schema:
            type: object
            properties:
              error:
                type: string
            required: [error]
  schemas:
    Board:
      type: object
      properties:
        id: { type: integer }
        name: { type: string }
        createdAt: { type: string, format: date-time }
        updatedAt: { type: string, format: date-time }
      required: [id, name, createdAt, updatedAt]
    Lane:
      type: object
      properties:
        id: { type: integer }
        boardId: { type: integer }
        name: { type: string }
        position: { type: integer }
      required: [id, boardId, name, position]
    Tag:
      type: object
      properties:
        id: { type: integer }
        boardId: { type: integer }
        name: { type: string }
        color: { type: string }
      required: [id, boardId, name, color]
    CommentSync:
      type: object
      properties:
        commentId: { type: integer }
        status:
          type: string
          enum: [local_only, pushing, pushed, push_failed]
        remoteCommentId:
          type: [string, "null"]
        pushedAt:
          type: [string, "null"]
          format: date-time
        lastError:
          type: [string, "null"]
        createdAt: { type: string, format: date-time }
        updatedAt: { type: string, format: date-time }
      required: [commentId, status, remoteCommentId, pushedAt, lastError, createdAt, updatedAt]
    Comment:
      type: object
      properties:
        id: { type: integer }
        ticketId: { type: integer }
        bodyMarkdown: { type: string }
        bodyHtml: { type: string }
        createdAt: { type: string, format: date-time }
        sync:
          $ref: "#/components/schemas/CommentSync"
      required: [id, ticketId, bodyMarkdown, bodyHtml, createdAt, sync]
    ActivityLog:
      type: object
      properties:
        id: { type: integer }
        boardId: { type: integer }
        ticketId:
          oneOf:
            - type: integer
            - type: "null"
        subjectTicketId: { type: integer }
        action: { type: string }
        message: { type: string }
        details:
          type: object
          additionalProperties: true
        createdAt: { type: string, format: date-time }
      required: [id, boardId, ticketId, subjectTicketId, action, message, details, createdAt]
    TicketEvent:
      type: object
      properties:
        id: { type: integer }
        ticketId: { type: integer }
        source:
          type: string
          description: Producer identifier such as a provider, agent, or integration.
        kind:
          type: string
          description: Source-defined event type.
        title: { type: string }
        summary:
          type: [string, "null"]
        severity:
          type: [string, "null"]
        icon:
          type: [string, "null"]
        data:
          type: object
          additionalProperties: true
        createdAt: { type: string, format: date-time }
      required: [id, ticketId, source, kind, title, summary, severity, icon, data, createdAt]
    TicketEventCreateBody:
      type: object
      properties:
        source: { type: string }
        kind: { type: string }
        title: { type: string }
        summary:
          type: [string, "null"]
        severity:
          type: [string, "null"]
        icon:
          type: [string, "null"]
        data:
          type: object
          additionalProperties: true
      required: [source, kind, title]
    TicketEventsResponse:
      type: object
      properties:
        events:
          type: array
          items:
            $ref: "#/components/schemas/TicketEvent"
      required: [events]
    TicketTagReason:
      type: object
      properties:
        tag:
          $ref: "#/components/schemas/Tag"
        reason:
          type: [string, "null"]
        details:
          type: [object, "null"]
          additionalProperties: true
        reasonCommentId:
          type: [integer, "null"]
        attachedAt:
          type: [string, "null"]
          format: date-time
        updatedAt:
          type: [string, "null"]
          format: date-time
      required: [tag, reason, details, reasonCommentId, attachedAt, updatedAt]
    TicketTagReasonsResponse:
      type: object
      properties:
        tags:
          type: array
          items:
            $ref: "#/components/schemas/TicketTagReason"
      required: [tags]
    TicketTagReasonSetBody:
      type: object
      properties:
        reason:
          type: [string, "null"]
        details:
          type: [object, "null"]
          additionalProperties: true
        reasonCommentId:
          type: [integer, "null"]
    TicketRelation:
      type: object
      properties:
        id: { type: integer }
        title: { type: string }
        laneId: { type: integer }
        isResolved:
          type: boolean
          description: Canonical resolved state. Use this instead of isCompleted.
        isCompleted:
          type: boolean
          deprecated: true
          description: Legacy alias for isResolved. Use isResolved for new clients.
        priority:
          type: integer
          enum: [1, 2, 3, 4]
          description: Ticket priority. 1 = low, 2 = medium, 3 = high, 4 = urgent.
        ref: { type: string }
        shortRef: { type: string }
      required: [id, title, laneId, isResolved, isCompleted, priority, ref, shortRef]
    TicketRelations:
      type: object
      properties:
        parent:
          oneOf:
            - $ref: "#/components/schemas/TicketRelation"
            - type: "null"
        children:
          type: array
          items:
            $ref: "#/components/schemas/TicketRelation"
        blockers:
          type: array
          items:
            $ref: "#/components/schemas/TicketRelation"
        blockedBy:
          type: array
          items:
            $ref: "#/components/schemas/TicketRelation"
        related:
          type: array
          items:
            $ref: "#/components/schemas/TicketRelation"
      required: [parent, children, blockers, blockedBy, related]
    TicketRemote:
      type: object
      properties:
        ticketId: { type: integer }
        provider: { type: string }
        instanceUrl: { type: string }
        resourceType: { type: string }
        projectKey: { type: string }
        issueKey: { type: string }
        displayRef: { type: string }
        url: { type: string }
        title: { type: string }
        bodyMarkdown:
          type: string
          description: Read-only remote issue body snapshot. The ticket bodyMarkdown remains the editable local implementation body.
        bodyHtml:
          type: string
          description: Sanitized HTML rendered from bodyMarkdown.
        state:
          type: [string, "null"]
        remoteUpdatedAt:
          type: [string, "null"]
          format: date-time
        lastSyncedAt: { type: string, format: date-time }
        createdAt: { type: string, format: date-time }
        updatedAt: { type: string, format: date-time }
      required: [ticketId, provider, instanceUrl, resourceType, projectKey, issueKey, displayRef, url, title, bodyMarkdown, bodyHtml, state, remoteUpdatedAt, lastSyncedAt, createdAt, updatedAt]
    TicketRemoteSummary:
      type: object
      properties:
        provider: { type: string }
        displayRef: { type: string }
        url: { type: string }
      required: [provider, displayRef, url]
    TicketExternalReference:
      type: object
      properties:
        id: { type: integer }
        ticketId: { type: integer }
        kind:
          type: string
          description: Reference purpose, such as source, spec, or doc.
        provider: { type: string }
        instanceUrl: { type: string }
        resourceType: { type: string }
        projectKey: { type: string }
        issueKey: { type: string }
        displayRef: { type: string }
        url: { type: string }
        title:
          type: [string, "null"]
        createdAt: { type: string, format: date-time }
        updatedAt: { type: string, format: date-time }
      required: [id, ticketId, kind, provider, instanceUrl, resourceType, projectKey, issueKey, displayRef, url, title, createdAt, updatedAt]
    TicketExternalReferenceSet:
      type: object
      properties:
        provider: { type: string }
        instanceUrl: { type: string }
        resourceType:
          type: string
          default: issue
        projectKey: { type: string }
        issueKey: { type: string }
        displayRef: { type: string }
        url: { type: string }
        title:
          type: [string, "null"]
      required: [provider, instanceUrl, projectKey, issueKey, displayRef, url]
    RemoteIssueImport:
      type: object
      properties:
        provider:
          type: string
          description: "Remote adapter name. Current providers: github, gitlab, redmine."
        laneId:
          type: integer
        instanceUrl:
          type: string
        projectKey:
          type: string
          description: Provider-specific project key, for example owner/repo for GitHub.
        issueKey:
          type: string
          description: Provider-specific issue key, for example issue number for GitHub.
        url:
          type: string
          description: Remote issue URL. For GitHub this can be used instead of projectKey and issueKey.
        postBacklinkComment:
          type: boolean
          description: When true, Kanbalone attempts to post one backlink comment to the remote issue after import. This is opt-in and requires provider comment/write permission.
        backlinkUrl:
          type: string
          description: Optional absolute http/https Kanbalone URL to include in the backlink comment. When omitted, the comment includes only the local ticket reference.
      required: [provider, laneId]
    RemoteIssueImportPreview:
      type: object
      properties:
        provider: { type: string }
        instanceUrl: { type: string }
        resourceType: { type: string }
        projectKey: { type: string }
        issueKey: { type: string }
        displayRef: { type: string }
        url: { type: string }
        title: { type: string }
        state:
          type: [string, "null"]
        remoteUpdatedAt:
          type: [string, "null"]
        duplicate:
          type: boolean
        existingTicketId:
          type: [integer, "null"]
        existingTicketRef:
          type: [string, "null"]
      required:
        - provider
        - instanceUrl
        - resourceType
        - projectKey
        - issueKey
        - displayRef
        - url
        - title
        - state
        - remoteUpdatedAt
        - duplicate
        - existingTicketId
        - existingTicketRef
    RemoteDiagnosticRequest:
      type: object
      properties:
        provider:
          type: string
          description: "Remote adapter name. Current providers: github, gitlab, redmine."
        instanceUrl:
          type: string
        projectKey:
          type: string
        issueKey:
          type: string
        url:
          type: string
          description: Remote issue URL to check.
      required: [provider]
    RemoteDiagnosticResult:
      type: object
      properties:
        provider:
          type: string
        hasCredential:
          type: boolean
        status:
          type: string
          enum: [missing_credential, reachable, auth_failed, permission_failed, not_found, rate_limited, unsupported_provider, error]
        displayRef:
          type: string
        url:
          type: string
        message:
          type: string
      required: [provider, hasCredential, status]
    Ticket:
      type: object
      properties:
        id: { type: integer }
        boardId: { type: integer }
        laneId: { type: integer }
        parentTicketId:
          type: [integer, "null"]
        hasChildren:
          type: boolean
          description: True when this ticket has one or more child tickets, independent of list filters.
        title: { type: string }
        bodyMarkdown: { type: string }
        bodyHtml: { type: string }
        isResolved:
          type: boolean
          description: Canonical resolved state. Use this instead of isCompleted.
        isCompleted:
          type: boolean
          deprecated: true
          description: Legacy alias for isResolved. Use isResolved for new clients.
        isArchived: { type: boolean }
        priority:
          type: integer
          enum: [1, 2, 3, 4]
          description: Ticket priority. 1 = low, 2 = medium, 3 = high, 4 = urgent.
        position: { type: integer }
        createdAt: { type: string, format: date-time }
        updatedAt: { type: string, format: date-time }
        tags:
          type: array
          items:
            $ref: "#/components/schemas/Tag"
        comments:
          type: array
          items:
            $ref: "#/components/schemas/Comment"
        blockerIds:
          type: array
          items:
            type: integer
        relatedIds:
          type: array
          items:
            type: integer
        blockers:
          type: array
          items:
            $ref: "#/components/schemas/TicketRelation"
        blockedBy:
          type: array
          items:
            $ref: "#/components/schemas/TicketRelation"
        related:
          type: array
          items:
            $ref: "#/components/schemas/TicketRelation"
        parent:
          oneOf:
            - $ref: "#/components/schemas/TicketRelation"
            - type: "null"
        children:
          type: array
          items:
            $ref: "#/components/schemas/TicketRelation"
        ref: { type: string }
        shortRef: { type: string }
        remote:
          oneOf:
            - $ref: "#/components/schemas/TicketRemote"
            - type: "null"
        externalReferences:
          type: array
          items:
            $ref: "#/components/schemas/TicketExternalReference"
      required:
        [id, boardId, laneId, parentTicketId, hasChildren, title, bodyMarkdown, bodyHtml, isResolved, isCompleted, isArchived, priority, position, createdAt, updatedAt, tags, comments, blockerIds, relatedIds, blockers, blockedBy, related, parent, children, ref, shortRef, remote, externalReferences]
    TicketSummary:
      type: object
      properties:
        id: { type: integer }
        boardId: { type: integer }
        laneId: { type: integer }
        parentTicketId:
          type: [integer, "null"]
        hasChildren:
          type: boolean
          description: True when this ticket has one or more child tickets, independent of list filters.
        title: { type: string }
        isResolved:
          type: boolean
          description: Canonical resolved state. Use this instead of isCompleted.
        isCompleted:
          type: boolean
          deprecated: true
          description: Legacy alias for isResolved. Use isResolved for new clients.
        isArchived: { type: boolean }
        priority:
          type: integer
          enum: [1, 2, 3, 4]
          description: Ticket priority. 1 = low, 2 = medium, 3 = high, 4 = urgent.
        position: { type: integer }
        createdAt: { type: string, format: date-time }
        updatedAt: { type: string, format: date-time }
        tags:
          type: array
          items:
            $ref: "#/components/schemas/Tag"
        blockerIds:
          type: array
          items:
            type: integer
        relatedIds:
          type: array
          items:
            type: integer
        ref: { type: string }
        shortRef: { type: string }
        remote:
          oneOf:
            - $ref: "#/components/schemas/TicketRemoteSummary"
            - type: "null"
        externalReferences:
          type: array
          items:
            $ref: "#/components/schemas/TicketExternalReference"
      required:
        [id, boardId, laneId, parentTicketId, hasChildren, title, isResolved, isCompleted, isArchived, priority, position, createdAt, updatedAt, tags, blockerIds, relatedIds, ref, shortRef, remote, externalReferences]
    TicketUpsert:
      type: object
      properties:
        laneId: { type: integer }
        parentTicketId:
          type: [integer, "null"]
        title: { type: string }
        bodyMarkdown: { type: string }
        isResolved:
          type: boolean
          description: Canonical resolved state. Use this instead of isCompleted.
        isCompleted:
          type: boolean
          deprecated: true
          description: Legacy alias for isResolved. Use isResolved for new clients.
        isArchived: { type: boolean }
        priority:
          type: integer
          enum: [1, 2, 3, 4]
          description: Ticket priority. 1 = low, 2 = medium, 3 = high, 4 = urgent.
        tagIds:
          type: array
          items:
            type: integer
        blockerIds:
          type: [array, "null"]
          items:
            type: integer
        relatedIds:
          type: [array, "null"]
          items:
            type: integer
    BoardShell:
      type: object
      properties:
        board:
          $ref: "#/components/schemas/Board"
        lanes:
          type: array
          items:
            $ref: "#/components/schemas/Lane"
        tags:
          type: array
          items:
            $ref: "#/components/schemas/Tag"
      required: [board, lanes, tags]
    BoardDetail:
      allOf:
        - $ref: "#/components/schemas/BoardShell"
        - type: object
          properties:
            tickets:
              type: array
              items:
                $ref: "#/components/schemas/Ticket"
          required: [tickets]
    BoardExport:
      type: object
      properties:
        board:
          $ref: "#/components/schemas/Board"
        lanes:
          type: array
          items:
            $ref: "#/components/schemas/Lane"
        tags:
          type: array
          items:
            $ref: "#/components/schemas/Tag"
        tickets:
          type: array
          items:
            type: object
      required: [board, lanes, tags, tickets]
