openapi: 3.1.0
info:
  title: Molted API
  version: 1.0.0
  description: |
    API to manage your OpenClaw instances on the Molted platform.

    ## Authentication

    The API supports two authentication methods:

    ### 1. API Key (recommended for integrations)

    Create an API key from the dashboard (Tenant Settings > API Keys) and send it in the `Authorization` header:

    ```
    Authorization: Bearer mk_live_xxxxxxxxxxxxxxxx
    ```

    Each API key has granular permissions. Available permissions:
    - `instances:read` : Read instance data
    - `instances:write` : Create, update, delete instances
    - `instances:operate` : Start, stop, restart instances
    - `instances:config` : Modify configuration (providers, config files, etc.)
    - `instances:logs` : Access logs
    - `instances:commands` : Send commands
    - `instance_tokens:write` : Create instance tokens
    - `billing:read` : Access billing data

    ### 2. JWT Session Token

    Token obtained after logging in via the dashboard. Send it in the `Authorization` header:

    ```
    Authorization: Bearer eyJhbGciOi...
    ```

    JWTs contain the `tenantId` which automatically scopes requests to your tenant.

    ## Base URL

    ```
    https://eu1.molted.net/v1
    ```

    ## Errors

    All errors follow the format:
    ```json
    {
      "statusCode": 400,
      "error": "Bad Request",
      "message": "Error description"
    }
    ```

servers:
  - url: https://eu1.molted.net/v1
    description: Production

security:
  - apiKey: []
  - bearerAuth: []

tags:
  - name: Instances
    description: Instance lifecycle management (create, update, delete, restart, reset)
  - name: Configuration
    description: OpenClaw configuration management with versioning
  - name: Files
    description: Instance file system access with versioning
  - name: Providers
    description: AI provider configuration (API keys, models)
  - name: Devices
    description: Device pairing management for an instance
  - name: Commands
    description: Send operational commands to instances
  - name: Logs
    description: Real-time and historical log access
  - name: Events
    description: Instance system events
  - name: Alerts
    description: Instance alerts and notifications
  - name: Analytics
    description: Tenant metrics and statistics
  - name: Billing
    description: Usage, invoices, and billing data
  - name: Tenant
    description: Tenant information
  - name: Users
    description: Tenant member management (owner only)
  - name: API Keys
    description: Machine-to-machine API key management (owner only)
  - name: Account
    description: Current user profile
  - name: Health
    description: API health status

paths:
  # ── Instances ──────────────────────────────────────────────
  /instances:
    get:
      tags: [Instances]
      summary: List instances
      description: Returns all active (non-deleted) instances for the authenticated tenant.
      operationId: listInstances
      responses:
        "200":
          description: Instance list
          content:
            application/json:
              schema:
                type: object
                properties:
                  instances:
                    type: array
                    items:
                      $ref: "#/components/schemas/Instance"

    post:
      tags: [Instances]
      summary: Create an instance
      operationId: createInstance
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/CreateInstanceRequest"
      responses:
        "201":
          description: Instance created
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Instance"
        "400":
          description: Invalid parameters
        "409":
          description: Handle already in use

  /instances/{instanceId}:
    parameters:
      - $ref: "#/components/parameters/instanceId"

    get:
      tags: [Instances]
      summary: Get instance details
      operationId: getInstance
      responses:
        "200":
          description: Instance details
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Instance"
        "404":
          description: Instance not found

    patch:
      tags: [Instances]
      summary: Update an instance
      operationId: updateInstance
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/UpdateInstanceRequest"
      responses:
        "200":
          description: Instance updated
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Instance"
        "404":
          description: Instance not found

    delete:
      tags: [Instances]
      summary: Delete an instance
      description: Soft delete. The instance is marked as deleted and its K8s resources are cleaned up.
      operationId: deleteInstance
      responses:
        "204":
          description: Instance deleted
        "404":
          description: Instance not found

  /instances/{instanceId}/restart:
    parameters:
      - $ref: "#/components/parameters/instanceId"
    post:
      tags: [Instances]
      summary: Restart an instance
      description: Deletes the current pod and creates a new one. The instance will be unavailable for a few seconds.
      operationId: restartInstance
      responses:
        "200":
          description: Restart initiated
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/OkResponse"
        "404":
          description: Instance not found

  /instances/{instanceId}/reset:
    parameters:
      - $ref: "#/components/parameters/instanceId"
    post:
      tags: [Instances]
      summary: Factory reset an instance
      description: |
        Purges all S3 data and resets retry counters.
        **The instance must be stopped** (`desiredState.pod = false`) before resetting.
      operationId: resetInstance
      responses:
        "200":
          description: Reset complete
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/OkResponse"
        "400":
          description: Instance must be stopped before reset
        "404":
          description: Instance not found

  /instances/{instanceId}/regen-gateway-token:
    parameters:
      - $ref: "#/components/parameters/instanceId"
    post:
      tags: [Instances]
      summary: Regenerate gateway token
      description: Generates a new dashboard gateway token and updates the instance configuration. Works for both running and stopped instances.
      operationId: regenGatewayToken
      responses:
        "200":
          description: Token regenerated
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:
                    type: boolean
                  gatewayToken:
                    type: string
                    description: New 48-character hex token
        "404":
          description: Instance not found

  /instances/{instanceId}/exec:
    parameters:
      - $ref: "#/components/parameters/instanceId"
    post:
      tags: [Instances]
      summary: Execute a command
      description: Runs a shell command inside the instance container. The instance must be running.
      operationId: execCommand
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [command]
              properties:
                command:
                  type: string
                  description: Command to execute
                  example: "ls -la /data"
                timeoutSeconds:
                  type: integer
                  minimum: 1
                  maximum: 300
                  default: 30
                  description: Timeout in seconds
      responses:
        "200":
          description: Execution result
          content:
            application/json:
              schema:
                type: object
                properties:
                  stdout:
                    type: string
                  stderr:
                    type: string
                  exitCode:
                    type: integer
        "400":
          description: Instance not running

  # ── Configuration ──────────────────────────────────────────
  /instances/{instanceId}/config:
    parameters:
      - $ref: "#/components/parameters/instanceId"

    get:
      tags: [Configuration]
      summary: Get current config
      description: Returns the current openclaw.json configuration. Reads from the live instance if running, otherwise from S3.
      operationId: getConfig
      responses:
        "200":
          description: Current configuration
          content:
            application/json:
              schema:
                type: object
                properties:
                  config:
                    type: object
                    description: Parsed JSON configuration (or raw string if not valid JSON)

    put:
      tags: [Configuration]
      summary: Update config
      description: Writes a new configuration to the instance. Instance must be running.
      operationId: updateConfig
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              description: Either a `content` string field, or a full JSON object that will be stringified
              properties:
                content:
                  type: string
                  description: Raw config content (takes priority if provided)
      responses:
        "200":
          description: Config updated
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/OkResponse"
        "400":
          description: Instance not running

  /instances/{instanceId}/config/history:
    parameters:
      - $ref: "#/components/parameters/instanceId"
    get:
      tags: [Configuration]
      summary: Config version history
      description: Returns all S3 versions of a config file.
      operationId: getConfigHistory
      parameters:
        - name: path
          in: query
          schema:
            type: string
            default: openclaw.json
          description: Config file path (no traversal allowed)
      responses:
        "200":
          description: Version list
          content:
            application/json:
              schema:
                type: object
                properties:
                  path:
                    type: string
                  versions:
                    type: array
                    items:
                      $ref: "#/components/schemas/S3Version"

  /instances/{instanceId}/config/file:
    parameters:
      - $ref: "#/components/parameters/instanceId"
    get:
      tags: [Configuration]
      summary: Get config at specific version
      operationId: getConfigVersion
      parameters:
        - name: versionId
          in: query
          required: true
          schema:
            type: string
          description: S3 version identifier
        - name: path
          in: query
          schema:
            type: string
            default: openclaw.json
      responses:
        "200":
          description: Config at requested version
          content:
            application/json:
              schema:
                type: object
                properties:
                  config:
                    type: object
                  versionId:
                    type: string
                  path:
                    type: string

  /instances/{instanceId}/config/diff:
    parameters:
      - $ref: "#/components/parameters/instanceId"
    get:
      tags: [Configuration]
      summary: Diff two config versions
      operationId: diffConfig
      parameters:
        - name: v1
          in: query
          required: true
          schema:
            type: string
          description: First S3 version ID
        - name: v2
          in: query
          required: true
          schema:
            type: string
          description: Second S3 version ID
        - name: path
          in: query
          schema:
            type: string
            default: openclaw.json
      responses:
        "200":
          description: Diff result
          content:
            application/json:
              schema:
                type: object
                properties:
                  v1:
                    type: string
                  v2:
                    type: string
                  path:
                    type: string
                  diff:
                    type: object
                    properties:
                      added:
                        type: array
                        items:
                          type: string
                      removed:
                        type: array
                        items:
                          type: string
                      changed:
                        type: array
                        items:
                          type: object
                          properties:
                            key:
                              type: string
                            old: {}
                            new: {}

  /instances/{instanceId}/config/restore:
    parameters:
      - $ref: "#/components/parameters/instanceId"
    post:
      tags: [Configuration]
      summary: Restore a config version
      description: Restores a previous config version. Instance must be running.
      operationId: restoreConfig
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [versionId]
              properties:
                versionId:
                  type: string
                  description: S3 version ID to restore
                path:
                  type: string
                  default: openclaw.json
      responses:
        "200":
          description: Config restored
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:
                    type: boolean
                  restoredVersion:
                    type: string
                  path:
                    type: string
        "400":
          description: Instance not running

  # ── Files ──────────────────────────────────────────────────
  /instances/{instanceId}/fs/tree:
    parameters:
      - $ref: "#/components/parameters/instanceId"
    get:
      tags: [Files]
      summary: List files
      description: Returns the file tree of the `.openclaw/` directory. If running, reads from the live instance; otherwise lists S3 objects.
      operationId: getFileTree
      responses:
        "200":
          description: File tree
          content:
            application/json:
              schema:
                type: array
                items:
                  type: object
                  properties:
                    path:
                      type: string
                    size:
                      type: integer
                    modTime:
                      type: string
                      format: date-time

  /instances/{instanceId}/fs/read:
    parameters:
      - $ref: "#/components/parameters/instanceId"
    get:
      tags: [Files]
      summary: Read a file
      operationId: readFile
      parameters:
        - name: path
          in: query
          required: true
          schema:
            type: string
          description: Relative file path (no traversal allowed)
      responses:
        "200":
          description: File content (Content-Type matches the file)
          content:
            application/octet-stream:
              schema:
                type: string
                format: binary
        "404":
          description: File not found

  /instances/{instanceId}/fs/write:
    parameters:
      - $ref: "#/components/parameters/instanceId"
    put:
      tags: [Files]
      summary: Write a file
      description: Instance must be running.
      operationId: writeFile
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [path, content]
              properties:
                path:
                  type: string
                  description: Relative file path
                content:
                  type: string
                  description: File content
      responses:
        "200":
          description: File written
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/OkResponse"
        "400":
          description: Instance not running

  /instances/{instanceId}/fs/delete:
    parameters:
      - $ref: "#/components/parameters/instanceId"
    delete:
      tags: [Files]
      summary: Delete a file
      description: Instance must be running.
      operationId: deleteFile
      parameters:
        - name: path
          in: query
          required: true
          schema:
            type: string
          description: Relative file path
      responses:
        "200":
          description: File deleted
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/OkResponse"
        "404":
          description: File not found

  /instances/{instanceId}/fs/history:
    parameters:
      - $ref: "#/components/parameters/instanceId"
    get:
      tags: [Files]
      summary: File version history
      operationId: getFileHistory
      parameters:
        - name: path
          in: query
          required: true
          schema:
            type: string
      responses:
        "200":
          description: Version list
          content:
            application/json:
              schema:
                type: object
                properties:
                  path:
                    type: string
                  versions:
                    type: array
                    items:
                      $ref: "#/components/schemas/S3Version"

  /instances/{instanceId}/fs/version:
    parameters:
      - $ref: "#/components/parameters/instanceId"
    get:
      tags: [Files]
      summary: Get file at specific version
      operationId: getFileVersion
      parameters:
        - name: path
          in: query
          required: true
          schema:
            type: string
        - name: versionId
          in: query
          required: true
          schema:
            type: string
      responses:
        "200":
          description: File content at requested version
          content:
            application/octet-stream:
              schema:
                type: string
                format: binary

  /instances/{instanceId}/fs/restore:
    parameters:
      - $ref: "#/components/parameters/instanceId"
    post:
      tags: [Files]
      summary: Restore a file version
      description: Instance must be running.
      operationId: restoreFile
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [path, versionId]
              properties:
                path:
                  type: string
                versionId:
                  type: string
      responses:
        "200":
          description: File restored
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:
                    type: boolean
                  restoredVersion:
                    type: string
                  path:
                    type: string
        "400":
          description: Instance not running

  # ── Providers ──────────────────────────────────────────────
  /providers:
    get:
      tags: [Providers]
      summary: List available providers
      description: Returns all supported AI providers. Public endpoint, no authentication required.
      operationId: listProviders
      security: []
      responses:
        "200":
          description: Provider list
          content:
            application/json:
              schema:
                type: object
                properties:
                  providers:
                    type: array
                    items:
                      $ref: "#/components/schemas/Provider"

  /providers/{providerId}/models:
    parameters:
      - name: providerId
        in: path
        required: true
        schema:
          type: string
        description: Provider ID
    get:
      tags: [Providers]
      summary: List models for a provider
      description: Returns available models grouped by category. Public endpoint.
      operationId: listProviderModels
      security: []
      responses:
        "200":
          description: Grouped model list
          content:
            application/json:
              schema:
                type: object
                properties:
                  groups:
                    type: array
                    items:
                      type: object
                      properties:
                        label:
                          type: string
                        models:
                          type: array
                          items:
                            type: object
                            properties:
                              id:
                                type: string
                              name:
                                type: string
                              context:
                                type: integer
                  source:
                    type: string
                    enum: [none, static, dynamic]

  /instances/{instanceId}/providers:
    parameters:
      - $ref: "#/components/parameters/instanceId"

    get:
      tags: [Providers]
      summary: List instance providers
      description: Credentials are masked (only last 4 characters visible).
      operationId: listInstanceProviders
      responses:
        "200":
          description: Configured providers
          content:
            application/json:
              schema:
                type: object
                properties:
                  providers:
                    type: array
                    items:
                      $ref: "#/components/schemas/InstanceProvider"

    post:
      tags: [Providers]
      summary: Add or update a provider
      operationId: setInstanceProvider
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/SetProviderRequest"
      responses:
        "200":
          description: Provider configured
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:
                    type: boolean
                  resolvedModel:
                    type: string
                    nullable: true

  /instances/{instanceId}/providers/{profileKey}:
    parameters:
      - $ref: "#/components/parameters/instanceId"
      - name: profileKey
        in: path
        required: true
        schema:
          type: string
        description: Provider profile key
    delete:
      tags: [Providers]
      summary: Remove a provider
      operationId: deleteInstanceProvider
      responses:
        "200":
          description: Provider removed
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/OkResponse"
        "404":
          description: Provider not found

  # ── Devices ────────────────────────────────────────────────
  /instances/{instanceId}/devices:
    parameters:
      - $ref: "#/components/parameters/instanceId"
    get:
      tags: [Devices]
      summary: List devices
      description: Returns paired and pending devices.
      operationId: listDevices
      responses:
        "200":
          description: Device list
          content:
            application/json:
              schema:
                type: object
                properties:
                  paired:
                    type: array
                    items:
                      $ref: "#/components/schemas/Device"
                  pending:
                    type: array
                    items:
                      $ref: "#/components/schemas/Device"

  /instances/{instanceId}/devices/{deviceId}/approve:
    parameters:
      - $ref: "#/components/parameters/instanceId"
      - $ref: "#/components/parameters/deviceId"
    post:
      tags: [Devices]
      summary: Approve a device
      operationId: approveDevice
      responses:
        "200":
          description: Device approved
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/OkResponse"

  /instances/{instanceId}/devices/{deviceId}/reject:
    parameters:
      - $ref: "#/components/parameters/instanceId"
      - $ref: "#/components/parameters/deviceId"
    post:
      tags: [Devices]
      summary: Reject a device
      operationId: rejectDevice
      responses:
        "200":
          description: Device rejected
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/OkResponse"

  /instances/{instanceId}/devices/{deviceId}:
    parameters:
      - $ref: "#/components/parameters/instanceId"
      - $ref: "#/components/parameters/deviceId"
    delete:
      tags: [Devices]
      summary: Remove a device
      operationId: deleteDevice
      responses:
        "200":
          description: Device removed
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/OkResponse"
        "404":
          description: Device not found

  # ── Commands ───────────────────────────────────────────────
  /instances/{instanceId}/commands:
    parameters:
      - $ref: "#/components/parameters/instanceId"

    get:
      tags: [Commands]
      summary: List commands
      description: Returns command history for the instance.
      operationId: listCommands
      responses:
        "200":
          description: Command list
          content:
            application/json:
              schema:
                type: object
                properties:
                  commands:
                    type: array
                    items:
                      $ref: "#/components/schemas/Command"

    post:
      tags: [Commands]
      summary: Send a command
      operationId: createCommand
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [action]
              properties:
                action:
                  type: string
                  enum:
                    - restart-openclaw
                    - restart-pod
                    - doctor
                    - "doctor --fix"
                    - config-reload
                    - shutdown
                    - pair
                  description: Action to execute
      responses:
        "201":
          description: Command queued
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/OkResponse"

  # ── Logs ───────────────────────────────────────────────────
  /instances/{instanceId}/logs:
    parameters:
      - $ref: "#/components/parameters/instanceId"
    get:
      tags: [Logs]
      summary: Stream live logs
      description: Server-Sent Events (SSE) stream of real-time logs. Instance must be running.
      operationId: streamLogs
      responses:
        "200":
          description: SSE stream
          content:
            text/event-stream:
              schema:
                type: string

  /instances/{instanceId}/logs/history:
    parameters:
      - $ref: "#/components/parameters/instanceId"
    get:
      tags: [Logs]
      summary: Get log history
      operationId: getLogHistory
      parameters:
        - name: lines
          in: query
          schema:
            type: integer
            default: 100
            maximum: 200
          description: Number of lines to return
      responses:
        "200":
          description: Log history
          content:
            application/json:
              schema:
                type: object
                properties:
                  lines:
                    type: array
                    items:
                      type: object
                      properties:
                        timestamp:
                          type: string
                        message:
                          type: string
                        level:
                          type: string

  # ── Events ─────────────────────────────────────────────────
  /instances/{instanceId}/events:
    parameters:
      - $ref: "#/components/parameters/instanceId"
    get:
      tags: [Events]
      summary: List events
      operationId: listEvents
      parameters:
        - name: type
          in: query
          schema:
            type: string
          description: Filter by event type (e.g. openclaw.crashed)
        - name: limit
          in: query
          schema:
            type: integer
            default: 50
            maximum: 200
        - name: offset
          in: query
          schema:
            type: integer
            default: 0
      responses:
        "200":
          description: Event list
          content:
            application/json:
              schema:
                type: object
                properties:
                  events:
                    type: array
                    items:
                      $ref: "#/components/schemas/Event"

  /instances/{instanceId}/events/stream:
    parameters:
      - $ref: "#/components/parameters/instanceId"
    get:
      tags: [Events]
      summary: Stream live events
      description: Server-Sent Events (SSE) stream with 30s heartbeat.
      operationId: streamEvents
      responses:
        "200":
          description: SSE stream
          content:
            text/event-stream:
              schema:
                type: string

  # ── Alerts ─────────────────────────────────────────────────
  /alerts:
    get:
      tags: [Alerts]
      summary: List alerts
      description: Returns alerts scoped to the authenticated tenant's instances.
      operationId: listAlerts
      parameters:
        - name: severity
          in: query
          schema:
            type: string
            enum: [critical, warning, info]
          description: Filter by severity
        - name: acknowledged
          in: query
          schema:
            type: string
            enum: ["true", "false"]
          description: Filter by acknowledgement status
        - name: limit
          in: query
          schema:
            type: integer
            default: 50
            maximum: 200
      responses:
        "200":
          description: Alert list
          content:
            application/json:
              schema:
                type: object
                properties:
                  alerts:
                    type: array
                    items:
                      $ref: "#/components/schemas/Alert"

  # ── Analytics ──────────────────────────────────────────────
  /analytics/tenants/{tenantId}/overview:
    parameters:
      - $ref: "#/components/parameters/tenantId"
    get:
      tags: [Analytics]
      summary: Tenant overview
      description: Global metrics for the tenant (instances, crashes, RAM, etc.).
      operationId: getTenantOverview
      responses:
        "200":
          description: Overview data
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/TenantOverview"

  /analytics/tenants/{tenantId}/channels:
    parameters:
      - $ref: "#/components/parameters/tenantId"
    get:
      tags: [Analytics]
      summary: Channel statistics
      operationId: getTenantChannels
      responses:
        "200":
          description: Channel distribution
          content:
            application/json:
              schema:
                type: object
                properties:
                  channels:
                    type: object
                    additionalProperties:
                      type: integer
                  totalInstances:
                    type: integer

  /analytics/tenants/{tenantId}/adoption:
    parameters:
      - $ref: "#/components/parameters/tenantId"
    get:
      tags: [Analytics]
      summary: Adoption statistics
      operationId: getTenantAdoption
      responses:
        "200":
          description: Adoption data
          content:
            application/json:
              schema:
                type: object
                properties:
                  instances:
                    type: array
                    items:
                      type: object
                      properties:
                        instanceId:
                          type: string
                        hostname:
                          type: string
                        adoptionLabel:
                          type: string
                          enum: [Active, Idle, Inactive]
                  totalInstances:
                    type: integer

  /analytics/tenants/{tenantId}/operations:
    parameters:
      - $ref: "#/components/parameters/tenantId"
    get:
      tags: [Analytics]
      summary: Operations statistics
      operationId: getTenantOperations
      parameters:
        - name: period
          in: query
          schema:
            type: string
          description: Analysis period
      responses:
        "200":
          description: Operations stats
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/OperationsStats"

  /analytics/tenants/{tenantId}/operations/feed:
    parameters:
      - $ref: "#/components/parameters/tenantId"
    get:
      tags: [Analytics]
      summary: Operations activity feed
      operationId: getTenantOperationsFeed
      parameters:
        - name: category
          in: query
          schema:
            type: string
            enum: [automated, manual, alert]
        - name: instanceId
          in: query
          schema:
            type: string
        - name: limit
          in: query
          schema:
            type: integer
            default: 50
            maximum: 200
        - name: offset
          in: query
          schema:
            type: integer
            default: 0
      responses:
        "200":
          description: Event feed
          content:
            application/json:
              schema:
                type: object
                properties:
                  events:
                    type: array
                    items:
                      $ref: "#/components/schemas/Event"
                  hasMore:
                    type: boolean
                  total:
                    type: integer

  # ── Billing ────────────────────────────────────────────────
  /billing/usage:
    get:
      tags: [Billing]
      summary: Current month usage
      description: Returns billing usage for the current period. Requires `billing:read` permission.
      operationId: getBillingUsage
      parameters:
        - name: tenantId
          in: query
          schema:
            type: string
          description: Tenant ID (only required for admin tokens)
      responses:
        "200":
          description: Usage data
          content:
            application/json:
              schema:
                type: object
                properties:
                  periodStart:
                    type: string
                    format: date
                  periodEnd:
                    type: string
                    format: date
                  daysInPeriod:
                    type: integer
                  daysElapsed:
                    type: integer
                  byInstanceType:
                    type: object
                    additionalProperties:
                      type: object
                      properties:
                        count:
                          type: integer
                        subtotal:
                          type: number
                  projectedTotal:
                    type: number
                    description: Projected end-of-month total (with tax)
                  estimatedTotal:
                    type: number
                    description: Estimated total if all current instances persist (with tax)
                  currency:
                    type: string
                    example: USD

  /billing/invoices:
    get:
      tags: [Billing]
      summary: List invoices
      operationId: listInvoices
      parameters:
        - name: tenantId
          in: query
          schema:
            type: string
          description: Tenant ID (only required for admin tokens)
        - name: limit
          in: query
          schema:
            type: integer
            default: 50
            maximum: 200
      responses:
        "200":
          description: Invoice list
          content:
            application/json:
              schema:
                type: object
                properties:
                  invoices:
                    type: array
                    items:
                      $ref: "#/components/schemas/Invoice"

  /billing/invoices/{invoiceId}:
    parameters:
      - name: invoiceId
        in: path
        required: true
        schema:
          type: string
    get:
      tags: [Billing]
      summary: Get invoice details
      operationId: getInvoice
      responses:
        "200":
          description: Invoice details
          content:
            application/json:
              schema:
                type: object
                properties:
                  invoice:
                    $ref: "#/components/schemas/Invoice"
        "403":
          description: Invoice belongs to another tenant
        "404":
          description: Invoice not found

  /billing/invoices/{invoiceId}/details:
    parameters:
      - name: invoiceId
        in: path
        required: true
        schema:
          type: string
    get:
      tags: [Billing]
      summary: Invoice line items by instance
      operationId: getInvoiceDetails
      parameters:
        - name: page
          in: query
          schema:
            type: integer
            default: 1
            minimum: 1
        - name: pageSize
          in: query
          schema:
            type: integer
            default: 100
            minimum: 1
            maximum: 500
      responses:
        "200":
          description: Invoice details by instance
          content:
            application/json:
              schema:
                type: object
                properties:
                  invoiceId:
                    type: string
                  instances:
                    type: array
                    items:
                      type: object
                      properties:
                        instanceId:
                          type: string
                        hostname:
                          type: string
                        instanceType:
                          type: string
                        billableFrom:
                          type: string
                          format: date
                        billableTo:
                          type: string
                          format: date
                        billableDays:
                          type: integer
                        prorata:
                          type: number
                        weightedEquivalent:
                          type: number
                  pagination:
                    type: object
                    properties:
                      total:
                        type: integer
                      page:
                        type: integer
                      pageSize:
                        type: integer

  # ── Tenant ─────────────────────────────────────────────────
  /tenants/{tenantId}:
    parameters:
      - $ref: "#/components/parameters/tenantId"
    get:
      tags: [Tenant]
      summary: Get tenant details
      operationId: getTenant
      responses:
        "200":
          description: Tenant information
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Tenant"
        "403":
          description: Access denied
        "404":
          description: Tenant not found

  # ── Users ──────────────────────────────────────────────────
  /tenants/{tenantId}/users:
    parameters:
      - $ref: "#/components/parameters/tenantId"

    get:
      tags: [Users]
      summary: List members
      description: Owner only.
      operationId: listTenantUsers
      responses:
        "200":
          description: Member list
          content:
            application/json:
              schema:
                type: object
                properties:
                  users:
                    type: array
                    items:
                      $ref: "#/components/schemas/TenantUser"

    post:
      tags: [Users]
      summary: Invite a member
      description: Owner only. Sends an invitation email.
      operationId: inviteTenantUser
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [email, name, role]
              properties:
                email:
                  type: string
                  format: email
                name:
                  type: string
                role:
                  type: string
                  enum: [owner, member, viewer]
      responses:
        "201":
          description: Invitation sent
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/TenantUser"

  /tenants/{tenantId}/users/{userId}:
    parameters:
      - $ref: "#/components/parameters/tenantId"
      - name: userId
        in: path
        required: true
        schema:
          type: string
    put:
      tags: [Users]
      summary: Update a member
      description: Owner only. Change a member's role.
      operationId: updateTenantUser
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                role:
                  type: string
                  enum: [owner, member, viewer]
                status:
                  type: string
      responses:
        "200":
          description: Member updated
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/TenantUser"

    delete:
      tags: [Users]
      summary: Remove a member
      description: Owner only. Disables the account and revokes all sessions.
      operationId: deleteTenantUser
      responses:
        "200":
          description: Member removed
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/OkResponse"

  # ── API Keys ───────────────────────────────────────────────
  /tenants/{tenantId}/api-keys:
    parameters:
      - $ref: "#/components/parameters/tenantId"

    get:
      tags: [API Keys]
      summary: List API keys
      description: Owner only.
      operationId: listApiKeys
      responses:
        "200":
          description: API key list
          content:
            application/json:
              schema:
                type: object
                properties:
                  apiKeys:
                    type: array
                    items:
                      $ref: "#/components/schemas/ApiKey"

    post:
      tags: [API Keys]
      summary: Create an API key
      description: |
        Owner only.
        **The full key is only displayed once in the response.** Store it immediately.
      operationId: createApiKey
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/CreateApiKeyRequest"
      responses:
        "201":
          description: Key created
          content:
            application/json:
              schema:
                type: object
                properties:
                  apiKey:
                    allOf:
                      - $ref: "#/components/schemas/ApiKey"
                      - type: object
                        properties:
                          key:
                            type: string
                            description: Full key (displayed once only)

  /tenants/{tenantId}/api-keys/{keyId}:
    parameters:
      - $ref: "#/components/parameters/tenantId"
      - name: keyId
        in: path
        required: true
        schema:
          type: string
    delete:
      tags: [API Keys]
      summary: Revoke an API key
      description: Owner only. Revocation is immediate and irreversible.
      operationId: revokeApiKey
      responses:
        "200":
          description: Key revoked
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/OkResponse"

  # ── Account ────────────────────────────────────────────────
  /auth/me:
    get:
      tags: [Account]
      summary: Get current user
      operationId: getMe
      responses:
        "200":
          description: Current user profile
          content:
            application/json:
              schema:
                type: object
                properties:
                  user:
                    type: object
                    properties:
                      _id:
                        type: string
                      email:
                        type: string
                        format: email
                      name:
                        type: string
                      type:
                        type: string
                        enum: [admin, tenant]
                      role:
                        type: string
                        nullable: true
                      tenantId:
                        type: string
                        nullable: true

    put:
      tags: [Account]
      summary: Update profile
      description: Update name and/or password. If changing password, all other sessions are revoked.
      operationId: updateMe
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                name:
                  type: string
                currentPassword:
                  type: string
                  description: Required when changing password
                newPassword:
                  type: string
                  minLength: 8
                  description: New password (minimum 8 characters)
      responses:
        "200":
          description: Profile updated
          content:
            application/json:
              schema:
                type: object
                properties:
                  user:
                    type: object
                    properties:
                      _id:
                        type: string
                      email:
                        type: string
                      name:
                        type: string
                      type:
                        type: string
                      role:
                        type: string
                        nullable: true
                      tenantId:
                        type: string
                        nullable: true
        "400":
          description: Validation error (missing currentPassword, password too short, etc.)
        "401":
          description: Current password incorrect

  # ── Health ─────────────────────────────────────────────────
  /health:
    get:
      tags: [Health]
      summary: API health check
      security: []
      operationId: healthCheck
      responses:
        "200":
          description: API operational
          content:
            application/json:
              schema:
                type: object
                properties:
                  status:
                    type: string
                    enum: [ok, degraded]
                  uptimeSeconds:
                    type: number
                  mongodb:
                    type: string
                    enum: [connected, disconnected]
        "503":
          description: API degraded

components:
  securitySchemes:
    apiKey:
      type: http
      scheme: bearer
      description: "API Key (mk_live_xxx or mk_test_xxx)"
    bearerAuth:
      type: http
      scheme: bearer
      bearerFormat: JWT
      description: "JWT session token"

  parameters:
    instanceId:
      name: instanceId
      in: path
      required: true
      schema:
        type: string
      description: Instance ID (format ins-xxxx)
    tenantId:
      name: tenantId
      in: path
      required: true
      schema:
        type: string
      description: Tenant ID (format tnt-xxxx)
    deviceId:
      name: deviceId
      in: path
      required: true
      schema:
        type: string
      description: Device ID

  schemas:
    OkResponse:
      type: object
      properties:
        ok:
          type: boolean
          example: true

    Instance:
      type: object
      properties:
        _id:
          type: string
          example: ins-a1b2c3d4e5f6
        tenantId:
          type: string
          example: tnt-e853649376a188
        handle:
          type: string
          example: my-instance
        domain:
          type: string
          example: molted.net
        hostname:
          type: string
          description: Alias for handle (deprecated)
        s3BucketId:
          type: string
        desiredState:
          $ref: "#/components/schemas/DesiredState"
        actualState:
          $ref: "#/components/schemas/ActualState"
        status:
          type: string
          enum: [deleted, stopped, starting, pod_only, running, stalled]
        instanceType:
          type: string
          default: standard
        createdAt:
          type: string
          format: date-time

    DesiredState:
      type: object
      properties:
        pod:
          type: boolean
          description: Should the pod exist?
        openclaw:
          type: boolean
          description: Should OpenClaw be running?
        openclawVersion:
          type: string
          default: latest
        resourcePriority:
          type: integer
          minimum: 1
          maximum: 10
          default: 5
          description: Resource priority (1 = low, 10 = high)

    ActualState:
      type: object
      properties:
        pod:
          type: boolean
        openclawRunning:
          type: boolean
        ramUsageMb:
          type: number
        openclawHealthy:
          type: boolean
        lastHeartbeat:
          type: string
          format: date-time
        openclawStatus:
          type: string
        diskUsagePercent:
          type: number

    CreateInstanceRequest:
      type: object
      required: [handle, tenantId]
      properties:
        handle:
          type: string
          description: DNS-compliant instance identifier (lowercase, alphanumeric, hyphens)
          pattern: "^[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?$"
          example: my-openclaw
        tenantId:
          type: string
          example: tnt-e853649376a188
        domain:
          type: string
          description: Custom domain (defaults to server config)
        openclawVersion:
          type: string
          default: latest
        resourcePriority:
          type: integer
          minimum: 1
          maximum: 10
          default: 5
        instanceType:
          type: string
          default: standard
        provider:
          type: object
          required: [id, credential]
          properties:
            id:
              type: string
              description: AI provider ID
              example: openrouter
            credential:
              type: string
              description: Provider API key or token
              example: sk-or-xxxx
            model:
              type: string
              description: Model to use
              example: google/gemini-2.5-flash
            thinkingLevel:
              type: string
              enum: [off, minimal, low, medium, high, xhigh, adaptive]
        channels:
          type: object
          properties:
            telegram:
              type: object
              properties:
                botToken:
                  type: string

    UpdateInstanceRequest:
      type: object
      properties:
        handle:
          type: string
          description: New handle
        desiredState:
          type: object
          properties:
            pod:
              type: boolean
            openclaw:
              type: boolean
            openclawVersion:
              type: string
            resourcePriority:
              type: integer
              minimum: 1
              maximum: 10

    S3Version:
      type: object
      properties:
        versionId:
          type: string
        lastModified:
          type: string
          format: date-time
        size:
          type: integer
        isLatest:
          type: boolean

    Device:
      type: object
      properties:
        deviceId:
          type: string
        publicKey:
          type: string
        platform:
          type: string
        clientId:
          type: string
        role:
          type: string
        approvedAt:
          type: integer
          description: Unix timestamp (seconds)

    Command:
      type: object
      properties:
        id:
          type: string
        action:
          type: string
        createdAt:
          type: string
          format: date-time
        status:
          type: string
          enum: [pending, done, failed]

    Event:
      type: object
      properties:
        _id:
          type: string
        instanceId:
          type: string
        type:
          type: string
          example: openclaw.crashed
        data:
          type: object
        nodeName:
          type: string
        createdAt:
          type: string
          format: date-time

    Alert:
      type: object
      properties:
        _id:
          type: string
        instanceId:
          type: string
        type:
          type: string
          example: disk_full
        severity:
          type: string
          enum: [critical, warning, info]
        message:
          type: string
        acknowledged:
          type: boolean
        createdAt:
          type: string
          format: date-time

    Provider:
      type: object
      properties:
        id:
          type: string
          example: openrouter
        name:
          type: string
          example: OpenRouter
        authType:
          type: string
          enum: [api_key, token]
        defaultModel:
          type: string
          nullable: true
        modelPrefix:
          type: string
          nullable: true
        requiresModel:
          type: boolean

    InstanceProvider:
      type: object
      properties:
        profileKey:
          type: string
        provider:
          type: string
        type:
          type: string
          enum: [api_key, token]
        credential:
          type: string
          description: Masked (last 4 characters only)
          example: "****xxxx"
        model:
          type: string
          nullable: true

    SetProviderRequest:
      type: object
      required: [provider, credential]
      properties:
        provider:
          type: string
          description: Provider ID
          example: openrouter
        credential:
          type: string
          description: API key or token
        model:
          type: string
          description: Model to use
        thinkingLevel:
          type: string
          enum: [off, minimal, low, medium, high, xhigh, adaptive]

    Tenant:
      type: object
      properties:
        _id:
          type: string
        name:
          type: string
        displayName:
          type: string
        plan:
          type: string
        createdAt:
          type: string
          format: date-time

    TenantUser:
      type: object
      properties:
        _id:
          type: string
        email:
          type: string
          format: email
        name:
          type: string
        type:
          type: string
          example: tenant
        tenantId:
          type: string
        role:
          type: string
          enum: [owner, member, viewer]
        status:
          type: string
          enum: [active, invited, disabled]
        createdAt:
          type: string
          format: date-time

    ApiKey:
      type: object
      properties:
        _id:
          type: string
        tenantId:
          type: string
        name:
          type: string
          example: CI/CD Pipeline
        prefix:
          type: string
          enum: [mk_live, mk_test]
        keyPreview:
          type: string
          description: Last 4 characters
          example: "ab12"
        permissions:
          type: array
          items:
            type: string
            enum:
              - instances:read
              - instances:write
              - instances:operate
              - instances:config
              - instances:logs
              - instances:commands
              - instance_tokens:write
              - billing:read
        createdBy:
          type: string
        lastUsedAt:
          type: string
          format: date-time
          nullable: true
        revoked:
          type: boolean
        createdAt:
          type: string
          format: date-time

    CreateApiKeyRequest:
      type: object
      required: [name, environment, permissions]
      properties:
        name:
          type: string
          minLength: 1
          maxLength: 100
          example: CI/CD Pipeline
        environment:
          type: string
          enum: [live, test]
        permissions:
          type: array
          minItems: 1
          items:
            type: string
            enum:
              - instances:read
              - instances:write
              - instances:operate
              - instances:config
              - instances:logs
              - instances:commands
              - instance_tokens:write
              - billing:read

    Invoice:
      type: object
      properties:
        _id:
          type: string
        tenantId:
          type: string
        periodStart:
          type: string
          format: date
        periodEnd:
          type: string
          format: date
        subtotal:
          type: number
        tax:
          type: number
        total:
          type: number
        status:
          type: string
          enum: [draft, sent, paid, failed]
        daysInPeriod:
          type: integer

    TenantOverview:
      type: object
      properties:
        runningInstances:
          type: integer
        totalInstances:
          type: integer
        totalCrashes30d:
          type: integer
        activeAlerts:
          type: integer
        avgRamMb:
          type: number
        computedAt:
          type: string
          format: date-time

    OperationsStats:
      type: object
      properties:
        period:
          type: object
          properties:
            start:
              type: string
              format: date-time
            end:
              type: string
              format: date-time
        stats:
          type: object
          properties:
            autoRecoveries:
              type: integer
            configRestores:
              type: integer
            doctorRepairs:
              type: integer
            ramPressureEvents:
              type: integer
            uptimePercent:
              type: number
        cumulative:
          type: object
          properties:
            totalIncidentsResolved:
              type: integer
            totalConfigRestores:
              type: integer
            totalDoctorRepairs:
              type: integer
            totalRamEvents:
              type: integer
            estimatedTimeSavedMinutes:
              type: integer
            memberSince:
              type: string
              format: date-time
