KindLM Cloud API — OpenAPI Specification

openapi: "3.1.0"
info:
  title: KindLM Cloud API
  version: "1.0.0"
  description: |
    REST API for KindLM Cloud. Provides persistent storage for test runs,
    baseline management, compliance reports, and team collaboration.
  contact:
    name: KindLM
    url: https://kindlm.com
    email: support@kindlm.com
  license:
    name: AGPL-3.0
    url: https://www.gnu.org/licenses/agpl-3.0.html

servers:
  - url: https://api.kindlm.com/v1
    description: Production
  - url: http://localhost:8787/v1
    description: Local development (Wrangler)

security:
  - BearerAuth: []

components:
  securitySchemes:
    BearerAuth:
      type: http
      scheme: bearer
      bearerFormat: "klm_*"
      description: |
        API token generated in dashboard or via `kindlm login`.
        Format: `klm_` prefix + 48 random hex characters.

  schemas:
    Error:
      type: object
      required: [error, message]
      properties:
        error:
          type: string
          description: Machine-readable error code
          example: "not_found"
        message:
          type: string
          description: Human-readable error message
          example: "Project not found"
        details:
          type: object
          description: Additional error context
          additionalProperties: true

    Organization:
      type: object
      required: [id, name, plan, created_at]
      properties:
        id:
          type: string
          example: "org_a1b2c3d4e5f6"
        name:
          type: string
          example: "ACME Corp"
        plan:
          type: string
          enum: [free, team, enterprise]
          example: "team"
        github_org:
          type: string
          nullable: true
          example: "acme-corp"
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time

    Project:
      type: object
      required: [id, org_id, name, created_at]
      properties:
        id:
          type: string
          example: "prj_x1y2z3"
        org_id:
          type: string
        name:
          type: string
          example: "customer-support-agent"
        description:
          type: string
          nullable: true
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time

    ProjectCreate:
      type: object
      required: [name]
      properties:
        name:
          type: string
          minLength: 1
          maxLength: 100
          pattern: "^[a-z0-9][a-z0-9-]*[a-z0-9]$"
        description:
          type: string
          maxLength: 500

    TestRun:
      type: object
      required: [id, project_id, total_tests, passed, failed, pass_rate, duration_ms, created_at]
      properties:
        id:
          type: string
          example: "run_m1n2o3"
        project_id:
          type: string
        git_commit:
          type: string
          nullable: true
          example: "a1b2c3d"
        git_branch:
          type: string
          nullable: true
          example: "main"
        ci_provider:
          type: string
          nullable: true
          enum: [github_actions, gitlab_ci, jenkins, circleci, local, null]
        total_tests:
          type: integer
          example: 24
        passed:
          type: integer
          example: 22
        failed:
          type: integer
          example: 2
        pass_rate:
          type: number
          format: float
          minimum: 0
          maximum: 1
          example: 0.917
        duration_ms:
          type: integer
          example: 12400
        compliance_report:
          type: string
          nullable: true
          description: Markdown compliance report content
        compliance_hash:
          type: string
          nullable: true
          description: SHA-256 hash of compliance report
        created_at:
          type: string
          format: date-time

    TestRunUpload:
      type: object
      required: [project_name, report]
      properties:
        project_name:
          type: string
          description: Project name (auto-created if not exists)
        report:
          type: object
          description: Full JSON report from `kindlm test --reporter json`
          required: [config_hash, suites, summary]
          properties:
            config_hash:
              type: string
            git:
              type: object
              properties:
                commit:
                  type: string
                branch:
                  type: string
                dirty:
                  type: boolean
            suites:
              type: array
              items:
                $ref: "#/components/schemas/SuiteResult"
            summary:
              $ref: "#/components/schemas/RunSummary"
            compliance:
              type: object
              nullable: true
              properties:
                markdown:
                  type: string
                hash:
                  type: string

    SuiteResult:
      type: object
      required: [name, tests]
      properties:
        name:
          type: string
        tests:
          type: array
          items:
            $ref: "#/components/schemas/TestResult"

    TestResult:
      type: object
      required: [name, pass, assertions]
      properties:
        name:
          type: string
          example: "refund-happy-path"
        pass:
          type: boolean
        assertions:
          type: array
          items:
            $ref: "#/components/schemas/AssertionResult"
        response_text:
          type: string
          nullable: true
        tool_calls:
          type: array
          items:
            type: object
            properties:
              name:
                type: string
              arguments:
                type: object
        latency_ms:
          type: integer
        cost_usd:
          type: number
          format: float

    AssertionResult:
      type: object
      required: [type, pass, message]
      properties:
        type:
          type: string
          enum: [tool_called, tool_not_called, tool_order, schema, judge, no_pii, keywords_present, keywords_absent, drift, latency, cost]
        pass:
          type: boolean
        score:
          type: number
          format: float
          nullable: true
          minimum: 0
          maximum: 1
        message:
          type: string
          example: "Tool lookup_order called with expected args"
        details:
          type: object
          nullable: true
          additionalProperties: true

    RunSummary:
      type: object
      required: [total_tests, passed, failed, pass_rate, duration_ms]
      properties:
        total_tests:
          type: integer
        passed:
          type: integer
        failed:
          type: integer
        pass_rate:
          type: number
          format: float
        duration_ms:
          type: integer
        total_cost_usd:
          type: number
          format: float
        gate_result:
          type: string
          enum: [passed, failed]

    RunComparison:
      type: object
      properties:
        current:
          $ref: "#/components/schemas/RunSummary"
        baseline:
          $ref: "#/components/schemas/RunSummary"
        deltas:
          type: object
          properties:
            pass_rate:
              type: number
              format: float
            duration_ms:
              type: integer
            cost_usd:
              type: number
              format: float
        test_diffs:
          type: array
          items:
            type: object
            properties:
              test_name:
                type: string
              status_changed:
                type: boolean
              current_pass:
                type: boolean
              baseline_pass:
                type: boolean
              drift_score:
                type: number
                format: float
                nullable: true

    Baseline:
      type: object
      required: [id, suite_name, label, created_at]
      properties:
        id:
          type: string
        suite_name:
          type: string
        label:
          type: string
          example: "v2.0-release"
        run_id:
          type: string
          description: The run this baseline was created from
        created_at:
          type: string
          format: date-time

    AuditLogEntry:
      type: object
      required: [id, actor, event, timestamp]
      properties:
        id:
          type: string
        actor:
          type: string
          description: User ID or "system"
        event:
          type: string
          enum: [run.uploaded, report.viewed, report.exported, baseline.set, member.added, member.removed, project.created, project.deleted, plan.changed]
        resource_type:
          type: string
          enum: [run, report, baseline, member, project, org]
        resource_id:
          type: string
        metadata:
          type: object
          additionalProperties: true
        timestamp:
          type: string
          format: date-time

    Pagination:
      type: object
      properties:
        total:
          type: integer
        page:
          type: integer
        per_page:
          type: integer
        has_more:
          type: boolean

  parameters:
    PageParam:
      name: page
      in: query
      schema:
        type: integer
        minimum: 1
        default: 1
    PerPageParam:
      name: per_page
      in: query
      schema:
        type: integer
        minimum: 1
        maximum: 100
        default: 20
    ProjectIdParam:
      name: project_id
      in: path
      required: true
      schema:
        type: string
    RunIdParam:
      name: run_id
      in: path
      required: true
      schema:
        type: string
    SuiteIdParam:
      name: suite_id
      in: path
      required: true
      schema:
        type: string

paths:
  # ── Auth ──
  /auth/github:
    post:
      tags: [Auth]
      summary: Exchange GitHub OAuth code for API token
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [code]
              properties:
                code:
                  type: string
                  description: GitHub OAuth authorization code
      responses:
        "200":
          description: Authentication successful
          content:
            application/json:
              schema:
                type: object
                properties:
                  token:
                    type: string
                    description: "API token (klm_* format)"
                  org:
                    $ref: "#/components/schemas/Organization"
        "401":
          description: Invalid OAuth code
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"

  /auth/verify:
    post:
      tags: [Auth]
      summary: Verify current token is valid
      responses:
        "200":
          description: Token is valid
          content:
            application/json:
              schema:
                type: object
                properties:
                  org:
                    $ref: "#/components/schemas/Organization"
                  user:
                    type: object
                    properties:
                      id:
                        type: string
                      github_login:
                        type: string
                      role:
                        type: string
                        enum: [owner, admin, member]
        "401":
          description: Invalid or expired token

  # ── Projects ──
  /projects:
    get:
      tags: [Projects]
      summary: List projects in current org
      parameters:
        - $ref: "#/components/parameters/PageParam"
        - $ref: "#/components/parameters/PerPageParam"
      responses:
        "200":
          description: Project list
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      $ref: "#/components/schemas/Project"
                  pagination:
                    $ref: "#/components/schemas/Pagination"

    post:
      tags: [Projects]
      summary: Create a project
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/ProjectCreate"
      responses:
        "201":
          description: Project created
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Project"
        "403":
          description: Project limit reached for current plan
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "409":
          description: Project name already exists in org

  /projects/{project_id}:
    get:
      tags: [Projects]
      summary: Get project details
      parameters:
        - $ref: "#/components/parameters/ProjectIdParam"
      responses:
        "200":
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Project"
        "404":
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"

    delete:
      tags: [Projects]
      summary: Delete a project and all associated runs
      parameters:
        - $ref: "#/components/parameters/ProjectIdParam"
      responses:
        "204":
          description: Deleted
        "404":
          description: Not found

  # ── Runs ──
  /projects/{project_id}/runs:
    get:
      tags: [Runs]
      summary: List test runs for a project
      parameters:
        - $ref: "#/components/parameters/ProjectIdParam"
        - $ref: "#/components/parameters/PageParam"
        - $ref: "#/components/parameters/PerPageParam"
        - name: branch
          in: query
          schema:
            type: string
        - name: since
          in: query
          schema:
            type: string
            format: date-time
        - name: until
          in: query
          schema:
            type: string
            format: date-time
      responses:
        "200":
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      $ref: "#/components/schemas/TestRun"
                  pagination:
                    $ref: "#/components/schemas/Pagination"

  /runs/upload:
    post:
      tags: [Runs]
      summary: Upload test results from CLI
      description: |
        Accepts the full JSON report from `kindlm test --reporter json`.
        Auto-creates the project if it doesn't exist (within plan limits).
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/TestRunUpload"
      responses:
        "201":
          description: Run uploaded
          content:
            application/json:
              schema:
                type: object
                properties:
                  run:
                    $ref: "#/components/schemas/TestRun"
                  dashboard_url:
                    type: string
                    format: uri
                    example: "https://cloud.kindlm.com/runs/run_m1n2o3"
        "403":
          description: Plan limit reached
        "413":
          description: Payload too large (max 5MB)

  /runs/{run_id}:
    get:
      tags: [Runs]
      summary: Get run details with all test results
      parameters:
        - $ref: "#/components/parameters/RunIdParam"
      responses:
        "200":
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/TestRun"
                  - type: object
                    properties:
                      results:
                        type: array
                        items:
                          $ref: "#/components/schemas/TestResult"

  /runs/{run_id}/compare:
    get:
      tags: [Runs]
      summary: Compare run against baseline or another run
      parameters:
        - $ref: "#/components/parameters/RunIdParam"
        - name: against
          in: query
          required: true
          schema:
            type: string
            description: "Run ID or 'baseline' or 'previous'"
      responses:
        "200":
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/RunComparison"

  # ── Baselines ──
  /projects/{project_id}/baselines:
    get:
      tags: [Baselines]
      summary: List baselines for a project
      parameters:
        - $ref: "#/components/parameters/ProjectIdParam"
      responses:
        "200":
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      $ref: "#/components/schemas/Baseline"

    post:
      tags: [Baselines]
      summary: Set a run as the new baseline
      parameters:
        - $ref: "#/components/parameters/ProjectIdParam"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [run_id, label]
              properties:
                run_id:
                  type: string
                label:
                  type: string
                  maxLength: 100
                suite_name:
                  type: string
                  description: Scope baseline to a specific suite
      responses:
        "201":
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Baseline"

  # ── Audit Log (Enterprise only) ──
  /audit-log:
    get:
      tags: [Audit]
      summary: Query audit log (Enterprise plan only)
      parameters:
        - name: since
          in: query
          schema:
            type: string
            format: date-time
        - name: until
          in: query
          schema:
            type: string
            format: date-time
        - name: event
          in: query
          schema:
            type: string
        - name: actor
          in: query
          schema:
            type: string
        - $ref: "#/components/parameters/PageParam"
        - $ref: "#/components/parameters/PerPageParam"
      responses:
        "200":
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      $ref: "#/components/schemas/AuditLogEntry"
                  pagination:
                    $ref: "#/components/schemas/Pagination"
        "403":
          description: Enterprise plan required

  # ── Health ──
  /health:
    get:
      tags: [System]
      summary: Health check
      security: []
      responses:
        "200":
          content:
            application/json:
              schema:
                type: object
                properties:
                  status:
                    type: string
                    example: "ok"
                  version:
                    type: string
                    example: "1.0.0"

Rate Limits

Rate limits are per-organization, enforced via Cloudflare Workers KV counter.

PlanRequests/hourBurst (per 10s)
Free10010
Team1,00050
Enterprise10,000200

Rate limit headers returned on every response:

X-RateLimit-Limit: 1000
X-RateLimit-Remaining: 847
X-RateLimit-Reset: 1708012800

When exceeded: 429 Too Many Requests with Retry-After header.

Error Codes

CodeHTTP StatusMeaning
unauthorized401Missing or invalid token
forbidden403Valid token but insufficient permissions
plan_required403Feature requires a higher plan
not_found404Resource doesn't exist
conflict409Duplicate resource (e.g., project name)
rate_limited429Too many requests
payload_too_large413Request body exceeds 5MB
validation_error422Invalid request body
internal_error500Unexpected server error

Idempotency

Upload endpoints accept an Idempotency-Key header. If the same key is sent twice within 24 hours, the second request returns the original response without creating a duplicate.

POST /v1/runs/upload
Idempotency-Key: ci-run-a1b2c3d4-2026-02-15