openapi: 3.1.0
info:
  title: POSnavigator Kanban Machine API
  version: "1.0.0"
  description: |-
    Automated kanban for scripts and agents. This is NOT the pn_
    X-Api-Key surface. Authenticate with Authorization: Bearer <kb.uuid.secret> or
    X-Kanban-Api-Key: <same>. Seed a key with node scripts/kanban-seed-api-key.mjs
    <role_key> (automation_sales, automation_marketing, automation_data_upload,
    automation_coordination, automation_development).
    
    Own-task rule: if task.assignee_key equals the key's role_key you may PATCH,
    comment, and soft-delete. Other people's tasks are GET + comment only (PATCH
    and soft-delete 403). Optimistic lock: PATCH and soft-delete require
    if_match_updated_at equal to the last seen updated_at; mismatch is 409
    CONFLICT_STALE_REVISION with the current task at the JSON root.
    
    status: idea|backlog|todo|in_progress|review|done. priority: low|normal|high|urgent.
    area: marketing|data_upload|sales|coordination|development. recurrence_interval
    null = one-off (Kanban columns); hourly|daily|weekly|monthly = recurring table.
    GET /board defaults to one-off only; include_recurring=1 adds recurring tasks.
    GET /board 400 BOARD_QUERY_TOO_LARGE above 5000 matching docs — use GET /tasks.
    
    Rate limit is IP+route (429 RATE_LIMITED), not the pn_ hourly window.
    
    # The happy path
    
    1. GET /openapi/kanban.yaml
    2. GET /api/kanban/v1/board?assignee_key=<your role_key>
    3. GET /api/kanban/v1/tasks/{id}
    4. PATCH with if_match_updated_at
    5. POST /comments; POST /soft-delete only on own tasks
    
    Snake_case on task objects.
servers:
  - url: https://posnavigator.eu
tags:
  - name: Discovery
  - name: Board
  - name: Tasks
security:
  - KanbanBearer: []
  - KanbanHeader: []
paths:
  /openapi/kanban.yaml:
    get:
      operationId: getKanbanOpenApi
      tags: [Discovery]
      security: []
      summary: Public OpenAPI 3.1 description
      description: |-
        This document. Public: no API key, no rate limit. Fetch it first and treat
        it as the complete manual. Every schema, limit, example, error code and
        header an agent needs is here. Linked from GET /openapi/catalog.yaml.
        Data operations below require authentication as described in info.
      responses:
        '200':
          description: OpenAPI YAML
          content:
            application/yaml:
              schema:
                type: string
              example: 'openapi: 3.1.0'
  /api/kanban/v1/board:
    get:
      operationId: getKanbanBoard
      tags: [Board]
      summary: Tasks grouped by status columns
      description: |-
        Soft-deleted excluded. Columns idea, backlog, todo, in_progress, review,
        done. Sort inside a column: priority, then due_at (nulls last), then
        updated_at desc. Not paginated. Default hides recurring tasks;
        include_recurring=1|true includes them. Optional assignee_key, area,
        priority, due_before, created_by_key. Over 5000 matches → 400
        BOARD_QUERY_TOO_LARGE with total_count and max_tasks.
      parameters:
        - name: assignee_key
          in: query
          schema: { type: string, maxLength: 100 }
        - name: area
          in: query
          schema: { type: string, enum: [marketing, data_upload, sales, coordination, development] }
        - name: priority
          in: query
          schema: { type: string, enum: [low, normal, high, urgent] }
        - name: due_before
          in: query
          schema: { type: string, maxLength: 40 }
        - name: created_by_key
          in: query
          schema: { type: string, maxLength: 100 }
        - name: include_recurring
          in: query
          schema: { type: string, enum: ['1', 'true'] }
      responses:
        '200':
          description: Board columns
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/KanbanOk'
        '400':
          description: BOARD_QUERY_TOO_LARGE or bad query
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/KanbanError'
        '401':
          description: Invalid or revoked kanban key
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/KanbanError'
        '429':
          description: RATE_LIMITED
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/KanbanError'
  /api/kanban/v1/tasks:
    get:
      operationId: listKanbanTasks
      tags: [Tasks]
      summary: Paginated task list
      description: |-
        Soft-deleted excluded. Filter assignee_key, status, status_in (comma
        list), area, priority, due_before, created_by_key. page default 1, limit
        default 50 max 100. Split recurring vs one-off client-side via
        recurrence_interval; there is no dedicated query flag on this list.
      parameters:
        - name: assignee_key
          in: query
          schema: { type: string, maxLength: 100 }
        - name: status
          in: query
          schema: { type: string, enum: [idea, backlog, todo, in_progress, review, done] }
        - name: status_in
          in: query
          schema: { type: string, maxLength: 80 }
        - name: area
          in: query
          schema: { type: string, enum: [marketing, data_upload, sales, coordination, development] }
        - name: priority
          in: query
          schema: { type: string, enum: [low, normal, high, urgent] }
        - name: page
          in: query
          schema: { type: integer, minimum: 1, default: 1 }
        - name: limit
          in: query
          schema: { type: integer, minimum: 1, maximum: 100, default: 50 }
      responses:
        '200':
          description: Task page
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/KanbanOk'
        '401':
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/KanbanError'
        '429':
          description: Rate limited
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/KanbanError'
    post:
      operationId: createKanbanTask
      tags: [Tasks]
      summary: Create a task
      description: |-
        Required title, description_html, status, assignee_key, created_by_key,
        priority, area. Optional due_at ISO or null, tags string[],
        recurrence_interval enum or null. 201 with data.task.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/KanbanTaskCreateRequest'
      responses:
        '201':
          description: Created
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/KanbanOk'
        '400':
          description: VALIDATION_ERROR
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/KanbanError'
        '401':
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/KanbanError'
  /api/kanban/v1/tasks/{id}:
    parameters:
      - name: id
        in: path
        required: true
        schema: { type: string, maxLength: 40 }
    get:
      operationId: getKanbanTask
      tags: [Tasks]
      summary: Get one task including comments
      description: |-
        Soft-deleted tasks are 404. data.task is snake_case including
        recurrence_interval and comments. Any valid kanban key may read.
      responses:
        '200':
          description: Task
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/KanbanOk'
        '400':
          description: Invalid id
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/KanbanError'
        '401':
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/KanbanError'
        '404':
          description: Not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/KanbanError'
    patch:
      operationId: patchKanbanTask
      tags: [Tasks]
      summary: Patch own task with optimistic lock
      description: |-
        Required if_match_updated_at plus at least one of title,
        description_html, status, assignee_key, priority, area, due_at, tags,
        recurrence_interval. Only the key's own tasks (assignee_key === role_key)
        may be patched — otherwise 403. Stale if_match_updated_at → 409 with
        current task at the root. recurrence_interval null converts back to
        one-off.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/KanbanTaskPatchRequest'
      responses:
        '200':
          description: Updated
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/KanbanOk'
        '400':
          description: Validation
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/KanbanError'
        '401':
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/KanbanError'
        '403':
          description: Not own task
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/KanbanError'
        '404':
          description: Not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/KanbanError'
        '409':
          description: CONFLICT_STALE_REVISION
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/KanbanConflict'
  /api/kanban/v1/tasks/{id}/comments:
    post:
      operationId: commentKanbanTask
      tags: [Tasks]
      summary: Comment on a task
      description: |-
        Any valid kanban key may comment, including on other assignees' tasks.
        Body { body: string, if_match_updated_at }. Optimistic lock still
        applies. Returns updated task.
      parameters:
        - name: id
          in: path
          required: true
          schema: { type: string, maxLength: 40 }
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/KanbanCommentRequest'
      responses:
        '200':
          description: Commented
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/KanbanOk'
        '400':
          description: Validation
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/KanbanError'
        '401':
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/KanbanError'
        '404':
          description: Not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/KanbanError'
        '409':
          description: Stale revision
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/KanbanConflict'
  /api/kanban/v1/tasks/{id}/soft-delete:
    post:
      operationId: softDeleteKanbanTask
      tags: [Tasks]
      summary: Soft-delete own task
      description: |-
        Marks your own task deleted. Only when assignee_key equals the calling
        key's role_key; otherwise 403. Body must include if_match_updated_at equal
        to the last seen updated_at; a mismatch is 409 CONFLICT_STALE_REVISION.
        Afterwards GET of the same id returns 404. Other people's tasks cannot be
        soft-deleted.
      parameters:
        - name: id
          in: path
          required: true
          schema: { type: string, maxLength: 40 }
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/KanbanSoftDeleteRequest'
      responses:
        '200':
          description: Soft-deleted
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/KanbanOk'
        '401':
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/KanbanError'
        '403':
          description: Not own task
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/KanbanError'
        '404':
          description: Not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/KanbanError'
        '409':
          description: Stale revision
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/KanbanConflict'
  /api/kanban/v1/tasks/{id}/events:
    get:
      operationId: listKanbanTaskEvents
      tags: [Tasks]
      summary: Activity log for a task
      description: |-
        created_at descending. event_type values include task_created,
        task_soft_deleted, status_changed, assignee_changed, title_changed,
        description_changed, priority_changed, area_changed, due_changed,
        tags_changed, recurrence_changed, comment_added. page default 1, limit
        default 30 max 50.
      parameters:
        - name: id
          in: path
          required: true
          schema: { type: string, maxLength: 40 }
        - name: page
          in: query
          schema: { type: integer, minimum: 1, default: 1 }
        - name: limit
          in: query
          schema: { type: integer, minimum: 1, maximum: 50, default: 30 }
      responses:
        '200':
          description: Events
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/KanbanOk'
        '401':
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/KanbanError'
        '404':
          description: Not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/KanbanError'
components:
  securitySchemes:
    KanbanBearer:
      type: http
      scheme: bearer
      description: |-
        Authorization: Bearer kb.<uuid>.<secret>. Do not send a pn_ X-Api-Key;
        Clerk would treat it as a session JWT on other routes, which is why
        /api/kanban/v1 bypasses Clerk and accepts this header here.
    KanbanHeader:
      type: apiKey
      in: header
      name: X-Kanban-Api-Key
      description: |-
        Alternative to Bearer. Same kb.<uuid>.<secret> value. Either header
        works; send one, not both. Seed with node scripts/kanban-seed-api-key.mjs.
  headers:
    X-RateLimit-Limit:
      description: Maximum requests allowed in the current window
      schema:
        type: integer
    X-RateLimit-Remaining:
      description: Requests remaining in the current window
      schema:
        type: integer
    X-RateLimit-Reset:
      description: Unix epoch seconds when the window resets
      schema:
        type: integer
    Retry-After:
      description: Seconds to wait before retrying a 429
      schema:
        type: integer
  parameters:
    BankId:
      name: bankId
      in: path
      required: true
      description: 24-character hex Mongo ObjectId of the bank
      schema:
        type: string
        pattern: '^[a-fA-F0-9]{24}$'
        maxLength: 24

  schemas:
    ErrorEnvelope:
      type: object
      additionalProperties: false
      required: [success, error]
      properties:
        success:
          type: boolean
          const: false
        error:
          type: object
          additionalProperties: false
          required: [code, message]
          properties:
            code:
              type: string
              maxLength: 80
              description: Stable machine code. Branch on this, never on message.
            message:
              type: string
              maxLength: 2000
            details:
              type: array
              maxItems: 50
              items:
                type: string
                maxLength: 500
            retryAfter:
              type: integer
              description: Present on RATE_LIMIT_EXCEEDED
            field_errors:
              type: object
              additionalProperties:
                type: array
                maxItems: 20
                items:
                  type: string
                  maxLength: 500
    LocalePair:
      type: object
      additionalProperties: false
      properties:
        hu:
          type: string
          maxLength: 20000
        en:
          type: string
          maxLength: 20000
    OffsetPagination:
      type: object
      additionalProperties: false
      required: [total, limit, offset, hasMore]
      properties:
        total:
          type: integer
        limit:
          type: integer
        offset:
          type: integer
        hasMore:
          type: boolean
    KanbanOk:
      type: object
      required: [success, data]
      additionalProperties: false
      properties:
        success: { type: boolean, const: true }
        data:
          type: object
          additionalProperties: true
    KanbanError:
      type: object
      required: [success, error]
      additionalProperties: true
      properties:
        success: { type: boolean, const: false }
        error:
          type: object
          additionalProperties: true
          properties:
            code: { type: string, maxLength: 80 }
            message: { type: string, maxLength: 2000 }
    KanbanConflict:
      type: object
      required: [success, error]
      additionalProperties: true
      properties:
        success: { type: boolean, const: false }
        error:
          type: object
          additionalProperties: true
          properties:
            code: { type: string, const: CONFLICT_STALE_REVISION }
            message: { type: string, maxLength: 2000 }
        task:
          type: object
          additionalProperties: true
          description: Current task; retry with its updated_at
    KanbanTaskCreateRequest:
      type: object
      required: [title, description_html, status, assignee_key, created_by_key, priority, area]
      additionalProperties: true
      properties:
        title: { type: string, maxLength: 300 }
        description_html: { type: string, maxLength: 50000 }
        status: { type: string, enum: [idea, backlog, todo, in_progress, review, done] }
        assignee_key: { type: string, maxLength: 100, description: User id or automation role_key }
        created_by_key: { type: string, maxLength: 100 }
        priority: { type: string, enum: [low, normal, high, urgent] }
        area: { type: string, enum: [marketing, data_upload, sales, coordination, development] }
        due_at: { type: [string, 'null'], maxLength: 40 }
        tags:
          type: array
          maxItems: 30
          items: { type: string, maxLength: 50 }
        recurrence_interval:
          type: [string, 'null']
          enum: [hourly, daily, weekly, monthly, null]
    KanbanTaskPatchRequest:
      type: object
      required: [if_match_updated_at]
      additionalProperties: true
      properties:
        if_match_updated_at:
          type: string
          maxLength: 40
          description: Last seen task.updated_at
        title: { type: string, maxLength: 300 }
        description_html: { type: string, maxLength: 50000 }
        status: { type: string, enum: [idea, backlog, todo, in_progress, review, done] }
        assignee_key: { type: string, maxLength: 100 }
        priority: { type: string, enum: [low, normal, high, urgent] }
        area: { type: string, enum: [marketing, data_upload, sales, coordination, development] }
        due_at: { type: [string, 'null'], maxLength: 40 }
        tags:
          type: array
          maxItems: 30
          items: { type: string, maxLength: 50 }
        recurrence_interval:
          type: [string, 'null']
          enum: [hourly, daily, weekly, monthly, null]
    KanbanCommentRequest:
      type: object
      required: [body, if_match_updated_at]
      additionalProperties: false
      properties:
        body: { type: string, maxLength: 8000 }
        if_match_updated_at: { type: string, maxLength: 40 }
    KanbanSoftDeleteRequest:
      type: object
      required: [if_match_updated_at]
      additionalProperties: false
      properties:
        if_match_updated_at: { type: string, maxLength: 40 }

