# Kiantu public API — developer surface.
# Kept in lockstep with https://kiantu.com/developers (update both or neither).
# This spec documents the supported integration surface, not every endpoint
# the first-party app uses.
openapi: 3.0.3
info:
  title: Kiantu API
  description: >
    Work intelligence for humans and AI agents. Authenticate with an API
    token from Settings → Security → API tokens, sent as a Bearer credential.
    Errors carry stable machine-readable codes. Rate limit: 600 req/min per
    member.
  version: "1.0"
  contact:
    email: hello@kiantu.com
servers:
  - url: https://api.kiantu.com/v1
security:
  - bearerAuth: []

paths:
  /activity/events:
    post:
      operationId: ingestActivityEvents
      summary: Push activity events (the universal ingestion port)
      description: >
        Accepts a single event object or `{"events": [...]}` with up to 100
        events. Unknown (source_type, event_type) pairs are accepted and
        stored. Retries with the same `external_id` deduplicate server-side.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              oneOf:
                - $ref: "#/components/schemas/ActivityEvent"
                - type: object
                  required: [events]
                  properties:
                    events:
                      type: array
                      maxItems: 100
                      items:
                        $ref: "#/components/schemas/ActivityEvent"
            examples:
              single:
                value:
                  source_type: custom
                  event_type: deploy_finished
                  external_id: deploy-2026-07-05-42
                  payload: { repo: acme/api, version: v1.4.2 }
      responses:
        "202":
          description: Events accepted
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: object
                    properties:
                      accepted: { type: integer }
                      deduplicated: { type: integer }
                      event_ids:
                        type: array
                        items: { type: string, format: uuid }
        "400": { $ref: "#/components/responses/Error" }
        "401": { $ref: "#/components/responses/Error" }

  /sessions:
    get:
      operationId: listSessions
      summary: List sessions
      parameters:
        - { name: state, in: query, schema: { type: string, enum: [active, paused, closed] } }
        - { name: from, in: query, schema: { type: string, format: date }, description: "Calendar date in your profile timezone" }
        - { name: to, in: query, schema: { type: string, format: date } }
        - { name: goal_id, in: query, schema: { type: string, format: uuid } }
        - { name: project_id, in: query, schema: { type: string, format: uuid } }
        - { name: limit, in: query, schema: { type: integer, maximum: 100 } }
        - { name: cursor, in: query, schema: { type: string }, description: "Opaque cursor from the previous page's meta.cursor" }
      responses:
        "200":
          description: Sessions
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items: { $ref: "#/components/schemas/Session" }
                  meta:
                    type: object
                    properties:
                      cursor: { type: string, nullable: true }
        "401": { $ref: "#/components/responses/Error" }
    post:
      operationId: createSession
      summary: Start a session (or record a past one)
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [intent_text, source]
              properties:
                intent_text: { type: string, maxLength: 500, description: "What you're working on" }
                source:
                  type: string
                  enum: [timer, manual]
                  description: >
                    `timer` starts a live session now (auto-closes any
                    previous one). `manual` records a finished block and
                    requires started_at + ended_at.
                goal_id: { type: string, format: uuid, nullable: true }
                task_id: { type: string, format: uuid, nullable: true }
                project_id: { type: string, format: uuid, nullable: true }
                estimated_duration_min: { type: integer, nullable: true }
                started_at: { type: string, format: date-time, nullable: true }
                ended_at: { type: string, format: date-time, nullable: true }
                idempotency_key: { type: string, nullable: true, description: "Same key returns the same session on retry" }
      responses:
        "201":
          description: Session created
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: { $ref: "#/components/schemas/Session" }
        "400": { $ref: "#/components/responses/Error" }
        "401": { $ref: "#/components/responses/Error" }

  /sessions/{session_id}/pause:
    post:
      operationId: pauseSession
      summary: Pause an active session
      parameters: [{ name: session_id, in: path, required: true, schema: { type: string, format: uuid } }]
      requestBody:
        required: false
        content:
          application/json:
            schema:
              type: object
              properties:
                pause_reason: { type: string, nullable: true }
      responses:
        "200": { description: Paused }
        "401": { $ref: "#/components/responses/Error" }
        "404": { $ref: "#/components/responses/Error" }
        "409": { $ref: "#/components/responses/Error" }

  /sessions/{session_id}/resume:
    post:
      operationId: resumeSession
      summary: Resume a paused session
      parameters: [{ name: session_id, in: path, required: true, schema: { type: string, format: uuid } }]
      responses:
        "200": { description: Resumed }
        "401": { $ref: "#/components/responses/Error" }
        "404": { $ref: "#/components/responses/Error" }
        "409": { $ref: "#/components/responses/Error" }

  /sessions/{session_id}/close:
    post:
      operationId: closeSession
      summary: Close a session (idempotent)
      parameters: [{ name: session_id, in: path, required: true, schema: { type: string, format: uuid } }]
      requestBody:
        required: false
        content:
          application/json:
            schema:
              type: object
              properties:
                closing_note: { type: string, nullable: true }
                closed_at: { type: string, format: date-time, nullable: true, description: "Back-date up to 24h; capped at the max session duration" }
      responses:
        "200": { description: Closed (already-closed sessions return their existing state) }
        "401": { $ref: "#/components/responses/Error" }
        "404": { $ref: "#/components/responses/Error" }

  /plate/assign:
    post:
      operationId: assignPlateItem
      summary: Put an item on a teammate's plate
      description: >
        Cross-member assignment. The assignee's daily capacity is respected —
        a full today overflows to the next day; the response says where the
        item landed.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [assignee_id, title]
              properties:
                assignee_id: { type: string, format: uuid }
                title: { type: string, maxLength: 500 }
                plate_date: { type: string, format: date, nullable: true, description: "Defaults to the assignee's today (their timezone)" }
      responses:
        "201":
          description: Assigned
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: object
                    properties:
                      plate_item_id: { type: string, format: uuid }
                      assignee_id: { type: string, format: uuid }
                      plate_date: { type: string, format: date }
                      overflowed: { type: boolean }
                      active_count: { type: integer }
                      capacity: { type: integer }
        "400": { $ref: "#/components/responses/Error" }
        "401": { $ref: "#/components/responses/Error" }

  /me:
    get:
      operationId: getMe
      summary: The member the token acts as
      responses:
        "200": { description: Profile, timezone, working hours }
        "401": { $ref: "#/components/responses/Error" }

components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      description: "API token (kt_…) from Settings → Security, or a session JWT."
  responses:
    Error:
      description: Error with a stable machine-readable code
      content:
        application/json:
          schema:
            type: object
            properties:
              error:
                type: object
                properties:
                  code: { type: string, example: SESSION_NOT_FOUND }
                  message: { type: string }
  schemas:
    ActivityEvent:
      type: object
      required: [event_type]
      properties:
        source_type:
          type: string
          default: custom
          pattern: "^[a-z0-9_.-]{1,64}$"
          description: "Origin slug, e.g. custom, vscode, desktop"
        event_type:
          type: string
          pattern: "^[a-z0-9_.-]{1,64}$"
        occurred_at:
          type: string
          format: date-time
          description: Defaults to now
        payload:
          type: object
          description: Opaque metadata (≤ 32 KB). No document bodies or secrets.
        external_id:
          type: string
          maxLength: 256
          description: Your stable id — dedup key across retries
    Session:
      type: object
      properties:
        session_id: { type: string, format: uuid }
        intent_text: { type: string }
        state: { type: string, enum: [active, paused, closed] }
        source: { type: string }
        work_type: { type: string, enum: [intentional, reactive] }
        goal_id: { type: string, format: uuid, nullable: true }
        project_id: { type: string, format: uuid, nullable: true }
        started_at: { type: string, format: date-time }
        closed_at: { type: string, format: date-time, nullable: true }
        user_tags: { type: array, items: { type: string } }
        created_at: { type: string, format: date-time }
