openapi: 3.1.0
info:
  title: Klang API
  version: '1.0.0'
  description: |
    Programmatic access to your Klang workspace.

    A "conversation" is the unit Klang stores: a title, an AI summary, and
    the **sources** the summary is derived from. A source is any piece of
    input attached to the conversation — most often a meeting transcript,
    plus any extras the user added (uploaded files, Klang-authored notes,
    pasted text). One call to `GET /api/v1/conversations/{id}` returns the
    title, the summary, and every source — including the flat-text
    transcript — so most integrations don't need to chain calls.

    ## Rate limits

    The API enforces a fixed-window rate limit per API key. Limits depend
    on the workspace's plan: freemium gets 50 requests/day, paid plans
    (Pro, Team) get 60 requests/minute. Every response carries
    `X-RateLimit-Limit`, `X-RateLimit-Remaining`, and `X-RateLimit-Reset`
    (Unix-seconds when the window rolls over). On a `429`, also expect a
    `Retry-After` header with seconds to wait. The official SDKs honour
    these headers and back off automatically.
  contact:
    name: Klang
    url: https://klang.ai
  license:
    name: Proprietary
servers:
  - url: https://app.klang.ai/api/v1
    description: Production
security:
  - bearerAuth: []
tags:
  - name: conversations
    description: Read conversations and their attached sources.
  - name: folders
    description: Read folders the API key's user can see.
  - name: identity
    description: Identify which user and workspace an API key is bound to.
  - name: bots
    description: Send the Klang bot to join and record a video meeting.
paths:
  /conversations:
    get:
      tags: [conversations]
      summary: List conversations
      operationId: listConversations
      description: |
        Returns conversations the API key's user can see, ordered by
        `created_at` descending (newest first). Each list entry contains a
        short summary and source metadata (no source content). Call
        `GET /conversations/{id}` to fetch the full payload including
        source content.
      parameters:
        - name: limit
          in: query
          description: Page size (max 100).
          schema:
            type: integer
            minimum: 1
            maximum: 100
            default: 25
        - name: cursor
          in: query
          description: Cursor returned from a previous list response.
          schema:
            type: string
        - name: folder_id
          in: query
          description: Restrict to conversations in this folder. By default this includes any descendant subfolder; pass `recursive=false` for direct children only.
          schema:
            type: string
        - name: recursive
          in: query
          description: When `folder_id` is set, controls whether descendants are included. Default `true` (any depth); set `false` to limit to conversations whose immediate folder is `folder_id`.
          schema:
            type: boolean
            default: true
        - name: status
          in: query
          description: |
            Filter by status. Defaults to `ready` so that polling integrations
            don't see in-progress or failed conversations. Pass `all` to
            include every status.
          schema:
            type: string
            enum: [ready, pending, error, all]
            default: ready
        - name: created_after
          in: query
          description: |
            Only return conversations created strictly after this timestamp.
            Use the `created_at` of the newest conversation from your previous
            sync as the next value. ISO 8601.
          schema:
            type: string
            format: date-time
      responses:
        '200':
          description: A page of conversations.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ConversationList'
              example:
                data:
                  - id: kw3pq2nyax7lr9d
                    title: Q2 roadmap planning
                    created_at: '2026-04-23T13:00:00Z'
                    updated_at: '2026-04-23T14:32:09Z'
                    status: ready
                    folder_id: '12'
                    digest: Q2 roadmap shipping May 28; Ada owns checklist
                    sources:
                      - id: bf6dxh4mvw9p8sk
                        type: transcript
                        duration_seconds: 3287
                        language: eng
                      - id: gz7tm1qj9c4xy5n
                        type: document
                  - id: mn8rf5tjzq2cv6h
                    title: Klang weekly sync
                    created_at: '2026-04-22T15:00:00Z'
                    updated_at: '2026-04-22T15:30:09Z'
                    status: ready
                    folder_id: '12'
                    digest: Eng team unblocking build pipeline regression
                    sources:
                      - id: cy2hk8vbpw5fqr3
                        type: transcript
                        duration_seconds: 1798
                        language: eng
                next_cursor: eyJpZCI6Im1uOHJmNXRqenEyY3Y2aCIsInRzIjoiMjAyNi0wNC0yMlQxNTowMDowMFoifQ
                has_more: true
        '401':
          $ref: '#/components/responses/Unauthorized'
        '429':
          $ref: '#/components/responses/RateLimited'
  /conversations/{id}:
    get:
      tags: [conversations]
      summary: Retrieve a conversation
      operationId: retrieveConversation
      description: |
        Returns the full conversation: metadata, the AI summary, and every
        attached source with its content rendered as text.
      parameters:
        - $ref: '#/components/parameters/ConversationId'
      responses:
        '200':
          description: The conversation.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ConversationDetail'
              example:
                id: kw3pq2nyax7lr9d
                title: Q2 roadmap planning
                created_at: '2026-04-23T13:00:00Z'
                updated_at: '2026-04-23T14:32:09Z'
                status: ready
                folder_id: '12'
                digest: Q2 roadmap shipping May 28; Ada owns checklist
                summary: |-
                  Team aligned on shipping the desktop client on May 28, three weeks later than planned.

                  ## Timeline
                  - Build pipeline regression cost a sprint
                  - Ada owns the launch checklist
                  - Ben is unblocking the pipeline
                sources:
                  - id: bf6dxh4mvw9p8sk
                    type: transcript
                    title: Q2 roadmap planning
                    added_at: '2026-04-23T13:00:00Z'
                    duration_seconds: 3287
                    language: eng
                    meeting_url: https://meet.google.com/abc-defg-hij
                    participants:
                      - name: Ada Lovelace
                      - name: Ben Cho
                    content: |-
                      [00:00:12] Ada Lovelace: Let's start with the timeline. We're three weeks behind on the desktop client.

                      [00:00:19] Ben Cho: Confirmed. The build pipeline regression cost us a sprint.
                  - id: gz7tm1qj9c4xy5n
                    type: document
                    title: Agenda
                    added_at: '2026-04-23T12:55:00Z'
                    content: |-
                      1. Timeline review
                      2. Marketing alignment
                      3. EU rollout
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/RateLimited'
  /me:
    get:
      tags: [identity]
      summary: Identify the API key
      operationId: retrieveMe
      description: |
        Returns the user and workspace the API key is bound to. Useful for
        confirming "which Klang account am I connected to" in OAuth-style
        connection UIs.
      responses:
        '200':
          description: Identity for the API key.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Identity'
              example:
                user:
                  email: ada@example.com
                workspace:
                  name: Acme
        '401':
          $ref: '#/components/responses/Unauthorized'
        '429':
          $ref: '#/components/responses/RateLimited'
  /folders:
    get:
      tags: [folders]
      summary: List folders
      operationId: listFolders
      description: |
        Returns folders the API key's user can see in the workspace,
        ordered by `created_at` descending. Use the returned `id` as
        `folder_id` on `GET /conversations` to filter conversations to a
        folder subtree.
      parameters:
        - name: limit
          in: query
          description: Page size (max 100).
          schema:
            type: integer
            minimum: 1
            maximum: 100
            default: 25
        - name: cursor
          in: query
          description: Cursor returned from a previous list response.
          schema:
            type: string
        - name: folder_id
          in: query
          description: Restrict to folders inside this folder. By default any descendant is returned; pass `recursive=false` for direct children only.
          schema:
            type: string
        - name: recursive
          in: query
          description: When `folder_id` is set, controls whether descendants are included. Default `true`; set `false` to limit to direct children of `folder_id`.
          schema:
            type: boolean
            default: true
      responses:
        '200':
          description: A page of folders.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/FolderList'
              example:
                data:
                  - id: '23'
                    name: Q2 prospects
                    parent_id: '18'
                    created_at: '2026-03-10T14:30:00Z'
                  - id: '18'
                    name: Sales calls
                    parent_id: null
                    created_at: '2026-02-01T12:00:00Z'
                  - id: '12'
                    name: Customer interviews
                    parent_id: null
                    created_at: '2026-01-15T09:00:00Z'
                next_cursor: null
                has_more: false
        '401':
          $ref: '#/components/responses/Unauthorized'
        '429':
          $ref: '#/components/responses/RateLimited'
  /bots:
    post:
      tags: [bots]
      summary: Send the Klang bot to a video meeting
      operationId: createBot
      description: |
        Asks the Klang recorder bot to join a video meeting (Google Meet,
        Microsoft Teams, or Zoom) and produce a transcript. The bot uses
        the workspace's configured display name and avatar.

        The endpoint returns immediately with the `id` of the conversation
        the transcript will land on. Wait for the `conversation.ready`
        webhook to fire, or poll `GET /api/v1/conversations/{id}` if you'd
        rather drive this synchronously.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [meeting_url, name]
              properties:
                meeting_url:
                  type: string
                  format: uri
                  description: |
                    Join URL for the meeting. Must be a Google Meet,
                    Microsoft Teams, or Zoom URL — other hosts return
                    `400`.
                name:
                  type: string
                  maxLength: 200
                  description: |
                    Title for the resulting conversation. Shown in the app
                    and used as the default `title` on the conversation.
                language:
                  type: string
                  description: |
                    Spoken language as ISO 639-3 (e.g. `eng`, `swe`), or
                    `auto` to let Klang detect it. Defaults to `auto`.
                folder_id:
                  type: string
                  description: |
                    Folder to save the conversation into. Defaults to the
                    user's default folder.
            example:
              meeting_url: https://meet.google.com/abc-defg-hij
              name: Q2 roadmap planning
              language: auto
              folder_id: '12'
      responses:
        '201':
          description: The bot has been queued.
          content:
            application/json:
              schema:
                type: object
                properties:
                  id:
                    type: string
                    description: ID of the conversation the transcript will land on.
                  meeting_url:
                    type: string
                  status:
                    type: string
                    enum: [pending]
              example:
                id: kw3pq2nyax7lr9d
                meeting_url: https://meet.google.com/abc-defg-hij
                status: pending
        '400':
          description: Invalid request — bad URL, unsupported host, or unknown folder.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          description: Meeting bot not enabled for this workspace.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '429':
          $ref: '#/components/responses/RateLimited'
components:
  parameters:
    ConversationId:
      name: id
      in: path
      required: true
      description: The conversation ID.
      schema:
        type: string
        example: kw3pq2nyax7lr9d
  responses:
    Unauthorized:
      description: Missing or invalid API key.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
    NotFound:
      description: Resource not found.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
    RateLimited:
      description: |
        Too many requests. The API enforces a fixed-window rate limit per API
        key, scaled by the workspace's plan (50/day freemium, 60/minute paid).
        Every response — success or failure — carries `X-RateLimit-Limit`,
        `X-RateLimit-Remaining`, and `X-RateLimit-Reset` (Unix-seconds when
        the window rolls over). On a `429`, `Retry-After` tells you how long
        to wait.
      headers:
        X-RateLimit-Limit:
          description: Requests allowed per window.
          schema: { type: integer }
        X-RateLimit-Remaining:
          description: Requests remaining in the current window.
          schema: { type: integer }
        X-RateLimit-Reset:
          description: Unix timestamp (seconds) when the current window rolls over.
          schema: { type: integer }
        Retry-After:
          description: Seconds to wait before retrying.
          schema: { type: integer }
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          example:
            error:
              type: rate_limited
              message: Free tier allows 50 requests/day. Upgrade to Pro for 60 requests/minute.
              tier: free_tier
              upgrade_url: https://klang.ai/upgrade
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      bearerFormat: API key
      description: "API key starting with `sk_…`, sent as `Authorization: Bearer sk_…`."
  schemas:
    Error:
      type: object
      properties:
        error:
          type: object
          properties:
            type:
              type: string
              nullable: true
            message:
              type: string
            tier:
              type: string
              description: Caller's plan tier (`free_tier`, `pro`, `enterprise`). Set on plan-related errors (rate limits, plan-gated features).
            upgrade_url:
              type: string
              format: uri
              description: Where to send the user to upgrade. Only included when an upgrade resolves the error.
    SourceLite:
      type: object
      description: |
        A piece of input attached to a conversation. Klang's summary and
        analysis are derived from the conversation's full set of sources —
        so this is what you read when you want to see what the AI was
        working from. Most conversations have a `transcript` as their
        primary source plus any extras the user attached. List responses
        return this minimal shape; use `GET /conversations/{id}` for the
        full version with `title`, `added_at`, `content`, and
        `participants`.
      properties:
        id: { type: string }
        type:
          type: string
          enum: [transcript, document, text, file]
          description: |
            What kind of input this source is.

            - `transcript` — what was said in a recorded meeting. The full
              speaker-labeled, time-stamped text is in `content` (on
              `Source`).
            - `document` — a document authored inside Klang (agendas,
              notes, decision logs, etc.).
            - `text` — a snippet of plain text the user pasted into the
              conversation.
            - `file` — an uploaded file (PDF, slide deck, Word doc).
              Extracted plain text is in `content`.
        duration_seconds:
          type: integer
          nullable: true
          description: Recording length. `null` for non-transcript sources.
        language:
          type: string
          nullable: true
          description: ISO 639-3 code (e.g. `eng`, `swe`). `null` for non-transcript sources.
    Source:
      type: object
      description: |
        A piece of input attached to a conversation. Klang's summary and
        analysis are derived from the conversation's full set of sources —
        so this is what you read when you want to see what the AI was
        working from. Most conversations have a `transcript` as their
        primary source plus any extras the user attached.

        Returned only by `GET /conversations/{id}`; list endpoints return
        the lighter `SourceLite` shape instead.
      properties:
        id: { type: string }
        type:
          type: string
          enum: [transcript, document, text, file]
          description: |
            What kind of input this source is.

            - `transcript` — what was said in a recorded meeting. The full
              speaker-labeled, time-stamped text is in `content`.
            - `document` — a document authored inside Klang (agendas,
              notes, decision logs, etc.).
            - `text` — a snippet of plain text the user pasted into the
              conversation.
            - `file` — an uploaded file (PDF, slide deck, Word doc).
              Extracted plain text is in `content`.
        title: { type: string, nullable: true }
        added_at: { type: string, format: date-time, nullable: true }
        duration_seconds:
          type: integer
          nullable: true
          description: Recording length. `null` for non-transcript sources.
        language:
          type: string
          nullable: true
          description: ISO 639-3 code (e.g. `eng`, `swe`). `null` for non-transcript sources.
        meeting_url:
          type: string
          nullable: true
          description: |
            Join URL of the video meeting the transcript was recorded from
            (Google Meet, Microsoft Teams, Zoom). `null` for transcripts
            from uploaded files and for non-transcript sources.
        content:
          type: string
          nullable: true
          description: |
            Plain-text rendering of the source. For transcripts this is
            a speaker-labeled, time-stamped flat string. May be `null` if
            content extraction failed for an uploaded file.
        participants:
          type: array
          description: |
            Speakers detected on the transcript. Empty array for
            non-transcript sources. Returns the speaker label as stored — a
            real name if the user has labeled the speaker in Klang, or a
            raw label like `Speaker 1` / `Talare 1` if not.
          items: { $ref: '#/components/schemas/Participant' }
    ConversationBase:
      type: object
      properties:
        id: { type: string }
        title: { type: string, nullable: true }
        created_at:
          type: string
          format: date-time
          description: |
            When the conversation was created in Klang. Use this as the
            next `created_after` value for incremental sync.
        updated_at: { type: string, format: date-time }
        status:
          type: string
          nullable: true
          enum: [pending, ready, error]
          description: |
            Processing state. Wait for `ready` before reading `summary`
            or transcript content.
        folder_id:
          type: string
          nullable: true
          description: |
            ID of the immediate folder containing the conversation. `null`
            for conversations at the workspace root.
        digest:
          type: string
          nullable: true
          description: |
            AI-generated short summary (≤80 characters) with the key names
            and topics, optimised to distinguish this conversation from
            similar ones. Present in both list and retrieve responses.
    ConversationListItem:
      allOf:
        - $ref: '#/components/schemas/ConversationBase'
        - type: object
          properties:
            sources:
              type: array
              items: { $ref: '#/components/schemas/SourceLite' }
    ConversationDetail:
      allOf:
        - $ref: '#/components/schemas/ConversationBase'
        - type: object
          properties:
            summary:
              type: string
              nullable: true
              description: |
                User-facing summary of the conversation: a short overview,
                then any topical sections and key principles as markdown
                bullets.
            sources:
              type: array
              items: { $ref: '#/components/schemas/Source' }
    ConversationList:
      type: object
      properties:
        data:
          type: array
          items: { $ref: '#/components/schemas/ConversationListItem' }
        next_cursor: { type: string, nullable: true }
        has_more: { type: boolean }
    Folder:
      type: object
      properties:
        id:
          type: string
          description: Folder identifier.
        name: { type: string }
        parent_id:
          type: string
          nullable: true
          description: ID of the parent folder, or null for top-level folders.
        created_at: { type: string, format: date-time }
    FolderList:
      type: object
      properties:
        data:
          type: array
          items: { $ref: '#/components/schemas/Folder' }
        next_cursor: { type: string, nullable: true }
        has_more: { type: boolean }
    Participant:
      type: object
      properties:
        name:
          type: string
          description: The speaker's name if labeled in Klang, otherwise the raw speaker label (e.g. `Speaker 1`).
    Identity:
      type: object
      properties:
        user:
          type: object
          properties:
            email: { type: string }
        workspace:
          type: object
          properties:
            name: { type: string }
