openapi: 3.1.0
info:
  title: Elva API Channel
  version: 1.0.0
  description: |
    Conversational REST API for company agents. The API is stateful: conversation
    history is maintained server-side, so each request carries only the new message and
    a conversation ID. Agent tools execute on the server.

    Authenticate with a per-company API key (Dashboard → Settings → API Keys),
    passed as `Authorization: Bearer be_<64 hex>`. Revocation is immediate.
servers:
  - url: https://{jambonzHost}
    variables:
      jambonzHost:
        default: your-jambonz-host.example.com
security:
  - companyApiKey: []

paths:
  /api/v1/conversations:
    post:
      summary: Start a conversation with an agent
      operationId: createConversation
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [agentId]
              properties:
                agentId:
                  type: string
                  format: uuid
                  description: Agent UUID. Must belong to the company that owns the API key.
                customSystemMessage:
                  type: string
                  minLength: 1
                  maxLength: 8000
                  description: >-
                    Added to the agent's system messages on EVERY turn of this conversation.
                    Additive — never replaces the agent's configured prompt.
                metadata:
                  type: object
                  additionalProperties: true
                  description: >-
                    Structured context injected into the agent's prompt as a
                    `CONVERSATION METADATA:` system message on every turn. Keep it small
                    (~2 KB). Update later with PATCH /conversations/{id}/metadata.
      responses:
        '201':
          description: Conversation created
          content:
            application/json:
              schema:
                type: object
                properties:
                  conversationId: { type: string }
                  agentId: { type: string, format: uuid }
                  createdAt: { type: string, format: date-time }
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '404': { $ref: '#/components/responses/NotFound' }

  /api/v1/conversations/{conversationId}/messages:
    post:
      summary: Send a message (sync by default, SSE with stream=true)
      operationId: sendMessage
      description: >-
        Send ONLY the new user message — the server loads the conversation history.
        The agent runs its full loop server-side (LLM turns + tool execution) before
        responding. Synchronous turns are capped at 120 seconds; prefer stream=true
        for long tool chains.
      parameters:
        - $ref: '#/components/parameters/conversationId'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [message]
              properties:
                message:
                  type: string
                  minLength: 1
                  maxLength: 32000
                customSystemMessage:
                  type: string
                  minLength: 1
                  maxLength: 8000
                  description: Added for THIS turn only, on top of the conversation-level one.
                customTools:
                  type: array
                  maxItems: 10
                  items: { $ref: '#/components/schemas/CustomTool' }
                stream:
                  type: boolean
                  default: false
                  description: When true, respond as text/event-stream (see SseEvent).
      responses:
        '200':
          description: Assistant reply (synchronous) or SSE stream (stream=true)
          content:
            application/json:
              schema:
                type: object
                properties:
                  conversationId: { type: string }
                  message:
                    type: object
                    properties:
                      role: { type: string, const: assistant }
                      content:
                        type: string
                        description: Full reply; multiple assistant messages joined with blank lines.
                  messages:
                    type: array
                    description: Individual assistant messages from this turn (tool-call continuations).
                    items:
                      type: object
                      properties:
                        role: { type: string, const: assistant }
                        content: { type: string }
                  toolCalls:
                    type: array
                    items: { type: string }
                    description: Names of tools the agent invoked this turn.
                  usage: { $ref: '#/components/schemas/Usage' }
            text/event-stream:
              schema: { $ref: '#/components/schemas/SseEvent' }
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '402':
          description: Insufficient AI credits
          content:
            application/json:
              schema:
                type: object
                properties:
                  error: { type: string }
                  minCredits: { type: number }
                  totalBalance: { type: number }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }
        '409':
          description: Conversation has ended, has no agent assigned, or its agent is no longer available
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Error' }
        '504':
          description: Agent turn exceeded the 120 s timeout
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Error' }

  /api/v1/conversations/{conversationId}/metadata:
    patch:
      summary: Replace conversation metadata
      operationId: updateConversationMetadata
      description: >-
        Replace the conversation's metadata (and optionally its conversation-level
        customSystemMessage). The new metadata fully replaces the previous metadata and is
        injected into the agent's prompt on every subsequent turn. No AI response is generated.
      parameters:
        - $ref: '#/components/parameters/conversationId'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [metadata]
              properties:
                metadata:
                  type: object
                  additionalProperties: true
                  description: Replaces the stored metadata wholesale.
                customSystemMessage:
                  type: string
                  minLength: 1
                  maxLength: 8000
                  description: When present, replaces the conversation-level custom system message.
      responses:
        '200':
          description: Metadata updated
          content:
            application/json:
              schema:
                type: object
                properties:
                  conversationId: { type: string }
                  metadata: { type: object, additionalProperties: true }
                  updatedAt: { type: string, format: date-time }
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }
        '409':
          description: Conversation has ended
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Error' }

  /api/v1/conversations/{conversationId}/manual:
    post:
      summary: Add a manual message without triggering an AI response
      operationId: addManualMessage
      description: >-
        Append a message from a human operator or external system to the conversation as
        an `admin` turn. No AI response is generated and no AI credits are consumed. The
        message becomes part of the history and is replayed into the agent's context on
        the next sendMessage turn (the model sees `[ADMIN MESSAGE - <username>] …`).
      parameters:
        - $ref: '#/components/parameters/conversationId'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [message]
              properties:
                message:
                  type: string
                  minLength: 1
                  maxLength: 32000
                username:
                  type: string
                  minLength: 1
                  maxLength: 120
                  description: Who added the message. Defaults to "admin" when blank or omitted.
                metadata:
                  type: object
                  additionalProperties: true
                  description: Stored alongside the message (e.g. department, ticketId).
      responses:
        '201':
          description: Manual message added
          content:
            application/json:
              schema:
                type: object
                properties:
                  conversationId: { type: string }
                  messageId: { type: string }
                  addedBy: { type: string }
                  timestamp: { type: string, format: date-time }
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }
        '409':
          description: Conversation has ended
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Error' }

  /api/v1/conversations/{conversationId}:
    get:
      summary: Get a conversation and its message history
      operationId: getConversation
      parameters:
        - $ref: '#/components/parameters/conversationId'
        - name: include
          in: query
          required: false
          schema:
            type: string
            enum: [all]
          description: "'all' also returns system/tool entries (default: user/assistant/admin)."
      responses:
        '200':
          description: Conversation with ordered messages
          content:
            application/json:
              schema:
                type: object
                properties:
                  conversationId: { type: string }
                  agentId: { type: [string, 'null'], format: uuid }
                  status: { type: [string, 'null'] }
                  createdAt: { type: [string, 'null'], format: date-time }
                  messages:
                    type: array
                    items:
                      type: object
                      properties:
                        role: { type: string, enum: [user, assistant, system, tool, admin] }
                        content: { type: string }
                        timestamp: { type: string, format: date-time }
                        metadata:
                          type: object
                          description: Present on admin (manual) messages — attribution only.
                          properties:
                            username: { type: string }
                            source: { type: string }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }

  /api/v1/conversations/{conversationId}/end:
    post:
      summary: End a conversation (transcripts are retained)
      operationId: endConversation
      parameters:
        - $ref: '#/components/parameters/conversationId'
      responses:
        '200':
          description: Conversation ended
          content:
            application/json:
              schema:
                type: object
                properties:
                  conversationId: { type: string }
                  status: { type: string, const: ended }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }

components:
  securitySchemes:
    companyApiKey:
      type: http
      scheme: bearer
      bearerFormat: be_<64 hex chars>
      description: Per-company API key from Dashboard → Settings → API Keys. Hash-stored; revocation is immediate.

  parameters:
    conversationId:
      name: conversationId
      in: path
      required: true
      schema: { type: string }
      description: The conversationId returned by createConversation.

  schemas:
    CustomTool:
      type: object
      description: >-
        Server-executable HTTP tool, additive to the agent's configured tools for this
        turn. The SERVER calls the URL with the LLM-chosen parameters — the client never
        implements or proxies tools. authConfig secrets are in-flight only: never
        persisted, never logged.
      required: [name, description, url, method]
      properties:
        name:
          type: string
          minLength: 1
          maxLength: 20
          pattern: '^[a-zA-Z0-9_-]+$'
          description: Exposed to the agent as custom_<conversationId>_<name>.
        description:
          type: string
          minLength: 1
          maxLength: 1024
        url:
          type: string
          format: uri
          description: HTTPS only. Supports {param} path placeholders.
        method:
          type: string
          enum: [GET, POST, PUT, DELETE]
        authType:
          type: string
          enum: [none, basic, apikey, bearer]
          default: none
        authConfig:
          type: object
          properties:
            username: { type: string }
            password: { type: string }
            headerName: { type: string }
            apiKey: { type: string }
            token: { type: string }
        parameters:
          type: array
          maxItems: 20
          items:
            type: object
            required: [name, type]
            properties:
              name: { type: string, minLength: 1, maxLength: 64 }
              type: { type: string, minLength: 1, maxLength: 20 }
              required: { type: boolean, default: false }
              description: { type: string, maxLength: 512 }

    Usage:
      type: object
      properties:
        promptTokens: { type: number }
        completionTokens: { type: number }
        totalTokens: { type: number }

    SseEvent:
      type: object
      description: >-
        One JSON object per `data:` line. type=content carries a text chunk;
        type=new_message marks a new assistant message after a tool run; type=tool names
        a server-side tool execution; type=done ends the turn (with usage);
        type=error ends the stream with an error message.
      required: [type]
      properties:
        type: { type: string, enum: [content, new_message, tool, done, error] }
        content: { type: string }
        name: { type: string }
        conversationId: { type: string }
        usage: { $ref: '#/components/schemas/Usage' }
        error: { type: string }

    Error:
      type: object
      properties:
        error: { type: string }
        details: { description: Present on validation errors (zod flatten output). }

  responses:
    BadRequest:
      description: Invalid request body or parameters
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Error' }
    Unauthorized:
      description: Missing, malformed, unknown, or revoked API key
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Error' }
    Forbidden:
      description: Resource belongs to a different company
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Error' }
    NotFound:
      description: Unknown conversation/agent, or not an API-channel session
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Error' }
