openapi: 3.1.0
info:
  title: Kol creator API
  version: 0.1.0
  summary: Make, test and publish AIs on Kol.
  description: |
    The HTTP API the `kol` CLI and MCP server use. Everything is JSON. Calls that change or read
    your AIs need a token from the sign-in flow (`POST /api/pair`, then approve the code in the
    Kol app, then `GET /api/pair/{code}`), sent as `Authorization: Bearer <token>`.

    Errors are `{"error": "...", "hint": "..."}` (hint is optional) with a 4xx or 5xx status.
  license:
    name: MIT
    identifier: MIT
servers:
  - url: https://portal.kol-ai.app
    description: Kol (placeholder address until launch)
  - url: http://localhost:8787
    description: The mock host (`npm run mock-host` in this repo)
security:
  - token: []
tags:
  - name: Sign-in
  - name: Rules
  - name: AIs
  - name: Testing
paths:
  /api/pair:
    post:
      tags: [Sign-in]
      summary: Start signing in
      description: |
        Returns a short `code` to approve in the Kol app (Companions → Creator portal), and a secret
        `deviceCode` that only you see. Codes expire after 5 minutes. At most 20 codes per address
        every 10 minutes.
      security: []
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                client: { type: string, enum: [cli], description: Ask for a CLI sign-in (with a device code). }
                label: { type: string, maxLength: 60, description: 'What is signing in, for example "kol on my-laptop".' }
            example: { client: cli, label: kol on my-laptop }
      responses:
        '200':
          description: A code to approve.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/PairStart' }
        '429': { $ref: '#/components/responses/TooMany' }
  /api/pair/{code}:
    get:
      tags: [Sign-in]
      summary: Collect the token
      description: Poll every 2 seconds until the code is approved. Needs the `deviceCode` from the start.
      security: []
      parameters:
        - { name: code, in: path, required: true, schema: { type: string, pattern: '^[A-Z0-9]{6}$' } }
        - { name: device, in: query, required: true, schema: { type: string }, description: The deviceCode from POST /api/pair. }
      responses:
        '200':
          description: Approved. Keep the token secret; it works for 90 days.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/PairDone' }
        '202':
          description: Not approved yet. Try again in 2 seconds.
          content:
            application/json:
              schema: { type: object, properties: { waiting: { type: boolean } } }
        '410':
          description: The code expired, was already used, or the device code is wrong.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Error' }
  /api/me:
    get:
      tags: [Sign-in]
      summary: Who you are
      responses:
        '200':
          description: You.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Me' }
        '401': { $ref: '#/components/responses/SignedOut' }
  /api/logout:
    post:
      tags: [Sign-in]
      summary: Sign out
      description: The token stops working.
      responses:
        '200': { description: Signed out. }
        '401': { $ref: '#/components/responses/SignedOut' }
  /api/schema:
    get:
      tags: [Rules]
      summary: Allowed values and limits
      security: []
      responses:
        '200':
          description: What a draft may contain.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Rules' }
  /api/validate:
    post:
      tags: [Rules]
      summary: Check a draft
      description: The same check as saving, without saving. `problem` is a short code, see the Draft schema.
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema: { type: object, required: [draft], properties: { draft: { $ref: '#/components/schemas/Draft' } } }
      responses:
        '200':
          description: The result.
          content:
            application/json:
              schema:
                type: object
                required: [ok]
                properties:
                  ok: { type: boolean }
                  problem: { type: string, enum: [malformed, name, tagline, persona, too long, look, price, kind, requestWhat] }
  /api/creations:
    get:
      tags: [AIs]
      summary: Your AIs
      responses:
        '200':
          description: Every AI you made.
          content:
            application/json:
              schema: { type: object, properties: { creations: { type: array, items: { $ref: '#/components/schemas/Creation' } } } }
        '401': { $ref: '#/components/responses/SignedOut' }
    post:
      tags: [AIs]
      summary: Make an AI
      description: Saves a new draft. Only you can see it until you publish it. At most 20 per person.
      requestBody:
        required: true
        content:
          application/json:
            schema: { type: object, required: [draft], properties: { draft: { $ref: '#/components/schemas/Draft' } } }
      responses:
        '200': { $ref: '#/components/responses/OneCreation' }
        '400': { $ref: '#/components/responses/Bad' }
        '401': { $ref: '#/components/responses/SignedOut' }
  /api/creations/{id}:
    parameters:
      - $ref: '#/components/parameters/Id'
    get:
      tags: [AIs]
      summary: One AI
      responses:
        '200': { $ref: '#/components/responses/OneCreation' }
        '404': { $ref: '#/components/responses/NotYours' }
    put:
      tags: [AIs]
      summary: Change an AI
      description: |
        Saves a new version. If it is published, everyone gets the change on their next call.
        `kind` can't change.
      requestBody:
        required: true
        content:
          application/json:
            schema: { type: object, required: [draft], properties: { draft: { $ref: '#/components/schemas/Draft' } } }
      responses:
        '200': { $ref: '#/components/responses/OneCreation' }
        '400': { $ref: '#/components/responses/Bad' }
        '404': { $ref: '#/components/responses/NotYours' }
  /api/creations/{id}/publish:
    parameters:
      - $ref: '#/components/parameters/Id'
    post:
      tags: [AIs]
      summary: Publish or unpublish
      description: 'Puts it in the marketplace (`on: true`) or takes it out. People who have it keep it.'
      requestBody:
        required: true
        content:
          application/json:
            schema: { type: object, required: [on], properties: { on: { type: boolean } } }
      responses:
        '200': { $ref: '#/components/responses/OneCreation' }
        '404': { $ref: '#/components/responses/NotYours' }
  /api/creations/{id}/knowledge:
    parameters:
      - $ref: '#/components/parameters/Id'
    put:
      tags: [AIs]
      summary: Replace its knowledge
      description: |
        Replaces everything it knows with these items. Web pages are read once, now. All or nothing:
        on an error nothing changes. At most 50 items and 40,000 characters in total.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [items]
              properties:
                items: { type: array, maxItems: 50, items: { $ref: '#/components/schemas/KnowledgeInput' } }
      responses:
        '200': { $ref: '#/components/responses/OneCreation' }
        '400': { $ref: '#/components/responses/Bad' }
        '404': { $ref: '#/components/responses/NotYours' }
  /api/creations/{id}/requests:
    parameters:
      - $ref: '#/components/parameters/Id'
    get:
      tags: [AIs]
      summary: Requests it took
      description: Requests callers confirmed and asked it to send you (the requests skill).
      responses:
        '200':
          description: Newest first.
          content:
            application/json:
              schema: { type: object, properties: { requests: { type: array, items: { $ref: '#/components/schemas/Request' } } } }
        '404': { $ref: '#/components/responses/NotYours' }
  /api/try:
    post:
      tags: [Testing]
      summary: Talk to an AI in text
      description: |
        The AI's next reply to a conversation, with the same instructions and skills it has in a call.
        Send a `draft` (with `knowledge`) to try something unsaved, or a saved `creation` ID.
        With no messages you get its greeting. Tools run in test mode: nothing is delivered.
        `dryRun` returns the instructions without calling the AI model.

        Runs on your own AI key. At most 120 per hour.
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/TryRequest' }
            example:
              draft: { name: Kitchen Buddy, tagline: Cooks with you., description: '', persona: You are Kitchen Buddy, a calm home cook., greeting: '', icon: restaurant, color: '#E8590C', voice: cedar, price: 0, canAnswerForYou: false, kind: companion }
              knowledge: [{ title: notes.md, text: Salt pasta water well. }]
              messages: [{ role: user, text: How much salt goes in pasta water? }]
      responses:
        '200':
          description: The reply, or with `dryRun` the instructions.
          content:
            application/json:
              schema:
                oneOf:
                  - { $ref: '#/components/schemas/TryReply' }
                  - { $ref: '#/components/schemas/DryRun' }
        '400': { $ref: '#/components/responses/Bad' }
        '402':
          description: You have no AI key. Add one in the Kol app (Companions → Your AI key).
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Error' }
        '404': { $ref: '#/components/responses/NotYours' }
        '429': { $ref: '#/components/responses/TooMany' }
        '502':
          description: The AI model did not answer, or your AI key was rejected.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Error' }
  /api/events:
    get:
      tags: [Testing]
      summary: Live events
      description: |
        Server-sent events (`text/event-stream`): each `data:` line is one JSON Event. Try-outs, voice
        tests from the app or portal, and new requests. A comment line every 20 seconds keeps it open.
      responses:
        '200':
          description: The stream.
          content:
            text/event-stream:
              schema: { $ref: '#/components/schemas/Event' }
        '401': { $ref: '#/components/responses/SignedOut' }
components:
  securitySchemes:
    token:
      type: http
      scheme: bearer
      description: A token from the sign-in flow. CLI tokens start with `kol_` and work for 90 days.
  parameters:
    Id:
      name: id
      in: path
      required: true
      schema: { type: string, pattern: '^[a-z0-9-]+$' }
      description: The AI's ID, like `c-1a2b3c4d`.
  responses:
    OneCreation:
      description: The AI.
      content:
        application/json:
          schema: { type: object, properties: { creation: { $ref: '#/components/schemas/Creation' } } }
    Bad:
      description: Something in the request is wrong; `error` says what.
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Error' }
    SignedOut:
      description: No token, or it expired or was revoked. Sign in again.
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Error' }
    NotYours:
      description: There is no such AI, or it isn't yours (`error` is "not yours").
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Error' }
    TooMany:
      description: Too many requests. Wait and try again.
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Error' }
  schemas:
    Error:
      type: object
      required: [error]
      properties:
        error: { type: string }
        hint: { type: string, description: What to do about it. }
    PairStart:
      type: object
      required: [code, link, expiresInSec]
      properties:
        code: { type: string, description: 6 characters to approve in the Kol app., examples: [K7Q4XZ] }
        link: { type: string, description: The same code as a link the phone can open (for a QR code)., examples: ['nokptt://pair/K7Q4XZ'] }
        expiresInSec: { type: integer, examples: [300] }
        deviceCode: { type: string, description: 'Only with client "cli": the secret needed to collect the token. Never show it to anyone.' }
    PairDone:
      type: object
      required: [token, me]
      properties:
        token: { type: string, description: Send as `Authorization Bearer`. }
        me: { $ref: '#/components/schemas/Person' }
        expiresAt: { type: string, format: date-time }
    Person:
      type: object
      properties:
        id: { type: string, description: Your 6-character Kol ID. }
        name: { type: string }
    Me:
      allOf:
        - $ref: '#/components/schemas/Person'
        - type: object
          properties:
            access:
              type: object
              description: Who pays for your try-outs and your public AIs' calls.
              properties:
                hasKey: { type: boolean, description: You added your own AI key in the app. }
                last4: { type: string }
                sponsored: { type: boolean, description: Kol pays (testers). }
    Rules:
      type: object
      properties:
        icons: { type: array, items: { type: string } }
        colors: { type: array, items: { type: string } }
        voices: { type: array, items: { type: string } }
        prices: { type: array, items: { type: number } }
        limits: { type: object, additionalProperties: { type: integer } }
        kinds: { type: array, items: { type: string } }
    Draft:
      type: object
      description: |
        What you fill in. The `kol` CLI makes this from `kol.json` and `persona.md`.
        Problem codes from validation: `name`, `tagline` (missing), `persona` (under 20 characters),
        `too long`, `look` (icon, color or voice not allowed), `price`, `kind`, `requestWhat`
        (requests on but nothing listed), `malformed` (a field missing or the wrong type).
      required: [name, tagline, description, persona, greeting, icon, color, voice, price, canAnswerForYou]
      properties:
        name: { type: string, maxLength: 24 }
        tagline: { type: string, maxLength: 80 }
        description: { type: string, maxLength: 400 }
        persona: { type: string, minLength: 20, maxLength: 2000, description: Its instructions. }
        greeting: { type: string, maxLength: 200, description: 'First words. {owner} is replaced by the name of who it talks to.' }
        icon: { type: string, enum: [sparkles, chatbubbles, restaurant, school, fitness, heart, musical-notes, book, briefcase, leaf, rocket, happy] }
        color: { type: string, enum: ['#3B5BDB', '#E8590C', '#2F9E44', '#C2255C', '#7048E8', '#0C8599', '#E67700', '#495057'] }
        voice: { type: string, enum: [marin, cedar, coral, sage, ash, verse, ballad, alloy] }
        price: { type: number, enum: [0, 0.99, 1.99, 2.99, 4.99], description: USD a month for a companion; 0 is free. }
        canAnswerForYou: { type: boolean, description: Companions only. It may answer calls for its owner and take a message. }
        kind: { type: string, enum: [companion, service], default: companion }
        takesRequests: { type: boolean, description: The requests skill. }
        requestWhat: { type: string, maxLength: 300, description: What a request must include. }
    Creation:
      type: object
      properties:
        id: { type: string }
        version: { type: integer, description: Goes up by one on every change. }
        draft: { $ref: '#/components/schemas/Draft' }
        published: { type: boolean }
        serviceId: { type: [string, 'null'], description: 'A service: its Kol ID, once it has been online.' }
        installs: { type: integer, description: People who have it (you included). }
        calls: { type: integer }
        minutes: { type: integer }
        requests: { type: integer }
        knowledge: { type: array, items: { $ref: '#/components/schemas/KnowledgeItem' } }
    KnowledgeItem:
      type: object
      properties:
        id: { type: string }
        kind: { type: string, enum: [text, file, url] }
        title: { type: string }
        chars: { type: integer }
        addedAt: { type: string, format: date-time }
    KnowledgeInput:
      oneOf:
        - type: object
          required: [kind, title, text]
          properties:
            kind: { type: string, enum: [text, file] }
            title: { type: string, maxLength: 80 }
            text: { type: string }
        - type: object
          required: [kind, url]
          properties:
            kind: { type: string, const: url }
            url: { type: string, format: uri, description: A public web page. Pages on private networks are refused. }
    Message:
      type: object
      required: [role, text]
      properties:
        role: { type: string, enum: [user, assistant] }
        text: { type: string, maxLength: 4000 }
    TryRequest:
      type: object
      properties:
        draft: { $ref: '#/components/schemas/Draft' }
        knowledge:
          type: array
          description: With `draft`. Text only (web pages are read when saved).
          items: { type: object, required: [title, text], properties: { title: { type: string }, text: { type: string } } }
        creation: { type: string, description: A saved AI's ID, instead of `draft`. }
        messages:
          type: array
          maxItems: 40
          description: The conversation so far, oldest first. The last one is from the user. Empty for the greeting.
          items: { $ref: '#/components/schemas/Message' }
        as: { type: string, enum: [owner, standIn], description: standIn tries it answering a call for its owner. }
        dryRun: { type: boolean }
    TryReply:
      type: object
      properties:
        as: { type: string, enum: [owner, service, standIn] }
        model: { type: string }
        reply: { type: string }
        tools:
          type: array
          items:
            type: object
            properties:
              tool: { type: string }
              args: { type: object }
              result: { type: string }
    DryRun:
      type: object
      properties:
        as: { type: string, enum: [owner, service, standIn] }
        instructions: { type: string, description: Everything the AI is told. }
        greeting: { type: string }
        tools: { type: array, items: { type: object, properties: { name: { type: string }, description: { type: string } } } }
    Request:
      type: object
      properties:
        id: { type: string }
        fromName: { type: string }
        at: { type: string, format: date-time }
        text: { type: string }
    Event:
      oneOf:
        - type: object
          description: A test or try-out line.
          properties:
            type: { const: test }
            call: { type: string }
            what: { type: string, enum: [start, end, heard, said, tool] }
            text: { type: string }
            tool: { type: string }
            args: { type: object }
            result: { type: string }
        - type: object
          description: A new request from a caller.
          properties:
            type: { const: request }
            message: { type: object }
        - type: object
          description: Your AIs changed (in the app or another window).
          properties:
            type: { const: changed }
