openapi: 3.1.0
info:
  title: Kovira Public API
  version: 1.0.0
  summary: Complete versioned HTTP contract for Kovira integrations and agents.
  description: |
    The canonical contract for every externally callable Kovira v1 surface.
    Every endpoint is hosted on `api.kovira.app` and grouped as
    `/v1/{major-group}/{operation}`. The schemas below cover customer REST and
    GraphQL, SCIM provisioning, ATLAS and VECTOR agents, provider callbacks,
    and token-authenticated calendar feeds.

    Create a public API key in Workspace Settings > API Keys and send it as a
    Bearer token for customer REST or GraphQL. Keys are tenant-bound,
    explicitly scoped, revocable, and limited to 60 requests per rolling
    minute. Treat pagination cursors as opaque.

    Authentication is surface-specific. SCIM uses a dedicated provisioning
    bearer. ATLAS and VECTOR use HMAC wire-v4 plus a persistent Ed25519 device
    identity. ATLAS credentials are rejected on VECTOR routes and VECTOR
    credentials are rejected on ATLAS routes; the authenticated key must also
    own the exact device registration. Provider callbacks verify Stripe, Slack,
    or Microsoft-issued signatures. A calendar feed's opaque path token is the
    credential.
  contact:
    name: Kovira API Support
    url: https://kovira.app/contact
  license:
    name: Proprietary
    url: https://kovira.app/terms
servers:
  - url: https://api.kovira.app/v1
    description: Production
externalDocs:
  description: Complete REST, GraphQL, ATLAS, and VECTOR handbook
  url: https://api.kovira.app/v1/docs
security:
  - publicApiKey: []
tags:
  - name: Discovery
    description: Version and documentation discovery.
  - name: Platform
    description: Unauthenticated service liveness.
  - name: Incidents
    description: Incident records and lifecycle transitions.
  - name: Configuration items
    description: Configuration management database records.
  - name: Layer 2
    description: Read-only network observations produced by VECTOR.
  - name: GraphQL
    description: Scoped GraphQL queries and incident mutations.
  - name: SCIM 2.0
    description: RFC 7644 identity discovery and provisioning.
  - name: ATLAS
    description: Signed endpoint inventory, telemetry, and lifecycle operations.
  - name: VECTOR
    description: Signed network discovery, configuration, and observation operations.
  - name: Integrations
    description: Provider-authenticated inbound callbacks.
  - name: Calendar
    description: High-entropy token-authenticated iCalendar feeds.

paths:
  /:
    get:
      tags: [Discovery]
      operationId: discoverApi
      summary: Discover the v1 API
      security: []
      responses:
        "200":
          description: API version and canonical documentation links.
          headers:
            Kovira-API-Version:
              $ref: "#/components/headers/ApiVersion"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/DiscoveryEnvelope"
        "405": { $ref: "#/components/responses/MethodNotAllowed" }

  /platform/health:
    get:
      tags: [Platform]
      operationId: getPlatformHealth
      summary: Check API liveness
      security: []
      responses:
        "200":
          description: The public API process is accepting requests.
          headers:
            Kovira-API-Version:
              $ref: "#/components/headers/ApiVersion"
            X-Request-ID:
              $ref: "#/components/headers/RequestId"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/HealthEnvelope"
        "405": { $ref: "#/components/responses/MethodNotAllowed" }

  /incidents:
    get:
      tags: [Incidents]
      operationId: listIncidents
      summary: List incidents
      description: Ordered by `(created_at, id)` descending.
      parameters:
        - $ref: "#/components/parameters/Limit100"
        - $ref: "#/components/parameters/Cursor"
        - in: query
          name: status
          schema: { $ref: "#/components/schemas/IncidentStatus" }
        - in: query
          name: priority
          schema: { type: string, enum: [p1, p2, p3, p4] }
      responses:
        "200":
          $ref: "#/components/responses/IncidentList"
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "429": { $ref: "#/components/responses/RateLimited" }
        "500": { $ref: "#/components/responses/InternalError" }
    post:
      tags: [Incidents]
      operationId: createIncident
      summary: Create an incident
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/IncidentCreate" }
      responses:
        "201":
          description: Incident created.
          headers:
            Kovira-API-Version: { $ref: "#/components/headers/ApiVersion" }
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/IncidentMutationEnvelope"
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "413": { $ref: "#/components/responses/PayloadTooLarge" }
        "429": { $ref: "#/components/responses/RateLimited" }
        "500": { $ref: "#/components/responses/InternalError" }

  /incidents/{id}:
    parameters:
      - $ref: "#/components/parameters/ResourceId"
    get:
      tags: [Incidents]
      operationId: getIncident
      summary: Get an incident
      responses:
        "200":
          description: Incident found.
          headers:
            Kovira-API-Version: { $ref: "#/components/headers/ApiVersion" }
          content:
            application/json:
              schema:
                type: object
                required: [data]
                properties:
                  data: { $ref: "#/components/schemas/Incident" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
        "429": { $ref: "#/components/responses/RateLimited" }
        "500": { $ref: "#/components/responses/InternalError" }
    patch:
      tags: [Incidents]
      operationId: updateIncident
      summary: Update incident fields
      description: Lifecycle status changes use `/incidents/{id}/transitions`.
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/IncidentUpdate" }
      responses:
        "200":
          description: Incident updated.
          headers:
            Kovira-API-Version: { $ref: "#/components/headers/ApiVersion" }
          content:
            application/json:
              schema: { $ref: "#/components/schemas/IncidentMutationEnvelope" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
        "413": { $ref: "#/components/responses/PayloadTooLarge" }
        "429": { $ref: "#/components/responses/RateLimited" }
        "500": { $ref: "#/components/responses/InternalError" }

  /incidents/{id}/transitions:
    parameters:
      - $ref: "#/components/parameters/ResourceId"
    post:
      tags: [Incidents]
      operationId: transitionIncident
      summary: Transition an incident
      description: Applies the server-enforced incident lifecycle state machine.
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/IncidentTransition" }
      responses:
        "200":
          description: Transition applied, or the requested state was already current.
          headers:
            Kovira-API-Version: { $ref: "#/components/headers/ApiVersion" }
          content:
            application/json:
              schema:
                type: object
                required: [data]
                properties:
                  data:
                    type: object
                    required: [id, number, status]
                    properties:
                      id: { type: string, format: uuid }
                      number: { type: string }
                      status: { $ref: "#/components/schemas/IncidentStatus" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
        "409": { $ref: "#/components/responses/Conflict" }
        "413": { $ref: "#/components/responses/PayloadTooLarge" }
        "429": { $ref: "#/components/responses/RateLimited" }
        "500": { $ref: "#/components/responses/InternalError" }

  /cis:
    get:
      tags: [Configuration items]
      operationId: listConfigurationItems
      summary: List configuration items
      description: Ordered by `(created_at, id)` descending across the CMDB projection.
      parameters:
        - $ref: "#/components/parameters/Limit100"
        - $ref: "#/components/parameters/Cursor"
        - in: query
          name: type
          schema: { $ref: "#/components/schemas/CIType" }
        - in: query
          name: status
          schema: { $ref: "#/components/schemas/CIStatus" }
      responses:
        "200": { $ref: "#/components/responses/CIList" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "429": { $ref: "#/components/responses/RateLimited" }
        "500": { $ref: "#/components/responses/InternalError" }
    post:
      tags: [Configuration items]
      operationId: createConfigurationItem
      summary: Create a configuration item
      description: Integration-managed, secret-bearing, and view-backed CI types are read-only.
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/CICreate" }
      responses:
        "201":
          description: CI created.
          headers:
            Kovira-API-Version: { $ref: "#/components/headers/ApiVersion" }
          content:
            application/json:
              schema: { $ref: "#/components/schemas/CIMutationEnvelope" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "413": { $ref: "#/components/responses/PayloadTooLarge" }
        "429": { $ref: "#/components/responses/RateLimited" }
        "500": { $ref: "#/components/responses/InternalError" }
        "503": { $ref: "#/components/responses/Unavailable" }

  /cis/{id}:
    parameters:
      - $ref: "#/components/parameters/ResourceId"
    get:
      tags: [Configuration items]
      operationId: getConfigurationItem
      summary: Get a configuration item
      responses:
        "200":
          description: CI found. Type-specific safe fields may also be present.
          headers:
            Kovira-API-Version: { $ref: "#/components/headers/ApiVersion" }
          content:
            application/json:
              schema:
                type: object
                required: [data]
                properties:
                  data: { $ref: "#/components/schemas/CI" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
        "429": { $ref: "#/components/responses/RateLimited" }
        "500": { $ref: "#/components/responses/InternalError" }
    patch:
      tags: [Configuration items]
      operationId: updateConfigurationItem
      summary: Update a configuration item
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/CIUpdate" }
      responses:
        "200":
          description: CI updated.
          headers:
            Kovira-API-Version: { $ref: "#/components/headers/ApiVersion" }
          content:
            application/json:
              schema: { $ref: "#/components/schemas/CIMutationEnvelope" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
        "409": { $ref: "#/components/responses/Conflict" }
        "413": { $ref: "#/components/responses/PayloadTooLarge" }
        "429": { $ref: "#/components/responses/RateLimited" }
        "500": { $ref: "#/components/responses/InternalError" }

  /l2/transceivers:
    get:
      tags: [Layer 2]
      operationId: listTransceivers
      summary: List transceiver inventory and latest lane-0 DDM
      parameters:
        - $ref: "#/components/parameters/Limit100"
        - $ref: "#/components/parameters/Cursor"
        - in: query
          name: form_factor
          schema: { type: string, maxLength: 40 }
        - in: query
          name: device_ci_id
          schema: { type: string, format: uuid }
      responses:
        "200":
          description: Cursor-paginated transceivers.
          headers:
            Kovira-API-Version: { $ref: "#/components/headers/ApiVersion" }
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/PageEnvelope"
                  - type: object
                    properties:
                      data:
                        type: array
                        items: { $ref: "#/components/schemas/Transceiver" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "429": { $ref: "#/components/responses/RateLimited" }
        "500": { $ref: "#/components/responses/InternalError" }

  /l2/anomalies:
    get:
      tags: [Layer 2]
      operationId: listNetworkAnomalies
      summary: List detected network anomalies
      parameters:
        - $ref: "#/components/parameters/Limit200"
        - $ref: "#/components/parameters/Cursor"
        - in: query
          name: status
          schema: { type: string, enum: [open, all], default: open }
        - in: query
          name: severity
          schema: { type: string, enum: [critical, warning, info] }
        - in: query
          name: anomaly_type
          schema: { $ref: "#/components/schemas/NetworkAnomalyType" }
        - in: query
          name: ci_id
          schema: { type: string, format: uuid }
      responses:
        "200":
          description: Cursor-paginated anomalies.
          headers:
            Kovira-API-Version: { $ref: "#/components/headers/ApiVersion" }
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/PageEnvelope"
                  - type: object
                    properties:
                      data:
                        type: array
                        items: { $ref: "#/components/schemas/NetworkAnomaly" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "429": { $ref: "#/components/responses/RateLimited" }
        "500": { $ref: "#/components/responses/InternalError" }

  /l2/cdp:
    get:
      tags: [Layer 2]
      operationId: listCdpNeighbours
      summary: List CDP neighbour observations
      parameters:
        - $ref: "#/components/parameters/Limit200"
        - $ref: "#/components/parameters/Cursor"
        - in: query
          name: local_switch_ci_id
          schema: { type: string, format: uuid }
        - in: query
          name: neighbor_device_id
          schema: { type: string, maxLength: 255 }
      responses:
        "200":
          description: Cursor-paginated CDP observations.
          headers:
            Kovira-API-Version: { $ref: "#/components/headers/ApiVersion" }
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/PageEnvelope"
                  - type: object
                    properties:
                      data:
                        type: array
                        items: { $ref: "#/components/schemas/CdpNeighbour" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "429": { $ref: "#/components/responses/RateLimited" }
        "500": { $ref: "#/components/responses/InternalError" }

  /graphql/query:
    get:
      tags: [GraphQL]
      operationId: executeGraphqlQueryByGet
      summary: Execute a GraphQL query over GET
      description: Intended for read-only queries. Production introspection is disabled and query depth is capped at eight.
      parameters:
        - in: query
          name: query
          required: true
          schema: { type: string, minLength: 1 }
        - in: query
          name: variables
          description: JSON-encoded variables object.
          schema: { type: string }
        - in: query
          name: operationName
          schema: { type: string }
      responses:
        "200": { $ref: "#/components/responses/GraphqlResult" }
        "400": { $ref: "#/components/responses/GraphqlResult" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "429": { $ref: "#/components/responses/RateLimited" }
        "500": { $ref: "#/components/responses/InternalError" }
    post:
      tags: [GraphQL]
      operationId: executeGraphqlOperation
      summary: Execute a GraphQL query or mutation
      description: Queries cover operational records; mutations are limited to incident creation, comments, and lifecycle transitions.
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/GraphqlRequest" }
      responses:
        "200": { $ref: "#/components/responses/GraphqlResult" }
        "400": { $ref: "#/components/responses/GraphqlResult" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "413": { $ref: "#/components/responses/PayloadTooLarge" }
        "429": { $ref: "#/components/responses/RateLimited" }
        "500": { $ref: "#/components/responses/InternalError" }

  /scim/v2/ServiceProviderConfig:
    get:
      tags: [SCIM 2.0]
      operationId: getScimServiceProviderConfig
      summary: Get supported SCIM capabilities
      security: [{ scimBearer: [] }]
      responses:
        "200": { $ref: "#/components/responses/ScimDocument" }
        "4XX": { $ref: "#/components/responses/ScimError" }
        "5XX": { $ref: "#/components/responses/ScimError" }

  /scim/v2/ResourceTypes:
    get:
      tags: [SCIM 2.0]
      operationId: listScimResourceTypes
      summary: List supported SCIM resource types
      security: [{ scimBearer: [] }]
      responses:
        "200": { $ref: "#/components/responses/ScimDocument" }
        "4XX": { $ref: "#/components/responses/ScimError" }
        "5XX": { $ref: "#/components/responses/ScimError" }

  /scim/v2/Schemas:
    get:
      tags: [SCIM 2.0]
      operationId: listScimSchemas
      summary: List supported SCIM schemas
      security: [{ scimBearer: [] }]
      responses:
        "200": { $ref: "#/components/responses/ScimDocument" }
        "4XX": { $ref: "#/components/responses/ScimError" }
        "5XX": { $ref: "#/components/responses/ScimError" }

  /scim/v2/Users:
    get:
      tags: [SCIM 2.0]
      operationId: listScimUsers
      summary: List or filter provisioned users
      security: [{ scimBearer: [] }]
      parameters:
        - $ref: "#/components/parameters/ScimFilter"
        - $ref: "#/components/parameters/ScimStartIndex"
        - $ref: "#/components/parameters/ScimCount"
      responses:
        "200":
          description: RFC 7644 ListResponse containing users.
          content:
            application/scim+json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/ScimListResponse"
                  - type: object
                    properties:
                      Resources:
                        type: array
                        items: { $ref: "#/components/schemas/ScimUser" }
        "4XX": { $ref: "#/components/responses/ScimError" }
        "5XX": { $ref: "#/components/responses/ScimError" }
    post:
      tags: [SCIM 2.0]
      operationId: createScimUser
      summary: Provision or link a user
      security: [{ scimBearer: [] }]
      requestBody:
        required: true
        content:
          application/scim+json:
            schema: { $ref: "#/components/schemas/ScimUserWrite" }
          application/json:
            schema: { $ref: "#/components/schemas/ScimUserWrite" }
      responses:
        "201":
          description: User provisioned.
          content:
            application/scim+json:
              schema: { $ref: "#/components/schemas/ScimUser" }
        "4XX": { $ref: "#/components/responses/ScimError" }
        "5XX": { $ref: "#/components/responses/ScimError" }

  /scim/v2/Users/{id}:
    parameters:
      - $ref: "#/components/parameters/ResourceId"
    get:
      tags: [SCIM 2.0]
      operationId: getScimUser
      summary: Get a provisioned user
      security: [{ scimBearer: [] }]
      responses:
        "200":
          description: User resource.
          content:
            application/scim+json:
              schema: { $ref: "#/components/schemas/ScimUser" }
        "4XX": { $ref: "#/components/responses/ScimError" }
        "5XX": { $ref: "#/components/responses/ScimError" }
    put:
      tags: [SCIM 2.0]
      operationId: replaceScimUser
      summary: Replace a provisioned user
      security: [{ scimBearer: [] }]
      requestBody:
        required: true
        content:
          application/scim+json:
            schema: { $ref: "#/components/schemas/ScimUserWrite" }
      responses:
        "200":
          description: Updated user.
          content:
            application/scim+json:
              schema: { $ref: "#/components/schemas/ScimUser" }
        "4XX": { $ref: "#/components/responses/ScimError" }
        "5XX": { $ref: "#/components/responses/ScimError" }
    patch:
      tags: [SCIM 2.0]
      operationId: patchScimUser
      summary: Apply SCIM PatchOp operations to a user
      security: [{ scimBearer: [] }]
      requestBody:
        required: true
        content:
          application/scim+json:
            schema: { $ref: "#/components/schemas/ScimPatchRequest" }
      responses:
        "200":
          description: Updated user.
          content:
            application/scim+json:
              schema: { $ref: "#/components/schemas/ScimUser" }
        "4XX": { $ref: "#/components/responses/ScimError" }
        "5XX": { $ref: "#/components/responses/ScimError" }
    delete:
      tags: [SCIM 2.0]
      operationId: deactivateScimUser
      summary: Deactivate a provisioned user
      security: [{ scimBearer: [] }]
      responses:
        "204": { description: User deactivated. }
        "4XX": { $ref: "#/components/responses/ScimError" }
        "5XX": { $ref: "#/components/responses/ScimError" }

  /scim/v2/Groups:
    get:
      tags: [SCIM 2.0]
      operationId: listScimGroups
      summary: List or filter provisioned groups
      security: [{ scimBearer: [] }]
      parameters:
        - $ref: "#/components/parameters/ScimFilter"
        - $ref: "#/components/parameters/ScimStartIndex"
        - $ref: "#/components/parameters/ScimCount"
      responses:
        "200":
          description: RFC 7644 ListResponse containing groups.
          content:
            application/scim+json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/ScimListResponse"
                  - type: object
                    properties:
                      Resources:
                        type: array
                        items: { $ref: "#/components/schemas/ScimGroup" }
        "4XX": { $ref: "#/components/responses/ScimError" }
        "5XX": { $ref: "#/components/responses/ScimError" }
    post:
      tags: [SCIM 2.0]
      operationId: createScimGroup
      summary: Provision a group
      security: [{ scimBearer: [] }]
      requestBody:
        required: true
        content:
          application/scim+json:
            schema: { $ref: "#/components/schemas/ScimGroupWrite" }
          application/json:
            schema: { $ref: "#/components/schemas/ScimGroupWrite" }
      responses:
        "201":
          description: Group provisioned.
          content:
            application/scim+json:
              schema: { $ref: "#/components/schemas/ScimGroup" }
        "4XX": { $ref: "#/components/responses/ScimError" }
        "5XX": { $ref: "#/components/responses/ScimError" }

  /scim/v2/Groups/{id}:
    parameters:
      - $ref: "#/components/parameters/ResourceId"
    get:
      tags: [SCIM 2.0]
      operationId: getScimGroup
      summary: Get a provisioned group
      security: [{ scimBearer: [] }]
      responses:
        "200":
          description: Group resource.
          content:
            application/scim+json:
              schema: { $ref: "#/components/schemas/ScimGroup" }
        "4XX": { $ref: "#/components/responses/ScimError" }
        "5XX": { $ref: "#/components/responses/ScimError" }
    put:
      tags: [SCIM 2.0]
      operationId: replaceScimGroup
      summary: Replace a group and optionally its membership
      security: [{ scimBearer: [] }]
      requestBody:
        required: true
        content:
          application/scim+json:
            schema: { $ref: "#/components/schemas/ScimGroupWrite" }
      responses:
        "200":
          description: Updated group.
          content:
            application/scim+json:
              schema: { $ref: "#/components/schemas/ScimGroup" }
        "4XX": { $ref: "#/components/responses/ScimError" }
        "5XX": { $ref: "#/components/responses/ScimError" }
    patch:
      tags: [SCIM 2.0]
      operationId: patchScimGroup
      summary: Apply SCIM PatchOp operations to a group
      security: [{ scimBearer: [] }]
      requestBody:
        required: true
        content:
          application/scim+json:
            schema: { $ref: "#/components/schemas/ScimPatchRequest" }
      responses:
        "200":
          description: Updated group.
          content:
            application/scim+json:
              schema: { $ref: "#/components/schemas/ScimGroup" }
        "4XX": { $ref: "#/components/responses/ScimError" }
        "5XX": { $ref: "#/components/responses/ScimError" }
    delete:
      tags: [SCIM 2.0]
      operationId: deleteScimGroup
      summary: Remove a provisioned group
      security: [{ scimBearer: [] }]
      responses:
        "204": { description: Group removed. }
        "4XX": { $ref: "#/components/responses/ScimError" }
        "5XX": { $ref: "#/components/responses/ScimError" }

  /atlas/ping:
    parameters: &atlasV4Headers
      - $ref: "#/components/parameters/AgentMachineId"
      - $ref: "#/components/parameters/AgentTimestamp"
      - $ref: "#/components/parameters/AgentNonce"
      - $ref: "#/components/parameters/AgentSignature"
      - $ref: "#/components/parameters/AgentSignatureVersion"
      - $ref: "#/components/parameters/AgentSignedPath"
      - $ref: "#/components/parameters/AgentKindAtlas"
      - $ref: "#/components/parameters/AgentVersion"
    get:
      tags: [ATLAS]
      operationId: pingAtlasAgentApi
      summary: Verify ATLAS connectivity and fleet credentials
      security: [{ atlasV4Key: [] }]
      responses: &agentResponses
        "200": { $ref: "#/components/responses/AgentJson" }
        "4XX": { $ref: "#/components/responses/AgentError" }
        "5XX": { $ref: "#/components/responses/AgentError" }

  /atlas/register:
    parameters: &atlasV4IdentityHeaders
      - $ref: "#/components/parameters/AgentMachineId"
      - $ref: "#/components/parameters/AgentTimestamp"
      - $ref: "#/components/parameters/AgentNonce"
      - $ref: "#/components/parameters/AgentSignature"
      - $ref: "#/components/parameters/AgentSignatureVersion"
      - $ref: "#/components/parameters/AgentSignedPath"
      - $ref: "#/components/parameters/AgentKindAtlas"
      - $ref: "#/components/parameters/AgentVersion"
      - $ref: "#/components/parameters/AgentIdentityProof"
    post:
      tags: [ATLAS]
      operationId: registerAtlasAgent
      summary: Enrol or rotate an ATLAS device identity
      description: First enrolment atomically consumes a 15-minute, single-use capability and binds the submitted Ed25519 key. Rotation requires proof from the currently registered identity and the exact same typed API-key binding.
      security: [{ atlasV4Key: [] }]
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/AtlasRegistrationRequest" }
      responses: *agentResponses

  /atlas/config:
    parameters: *atlasV4IdentityHeaders
    get:
      tags: [ATLAS]
      operationId: getAtlasConfig
      summary: Get the current ATLAS schedule and policy
      security: [{ atlasV4Key: [] }]
      responses: *agentResponses

  /atlas/telemetry:
    parameters:
      - $ref: "#/components/parameters/AgentMachineId"
      - $ref: "#/components/parameters/AgentTimestamp"
      - $ref: "#/components/parameters/AgentNonce"
      - $ref: "#/components/parameters/AgentSignature"
      - $ref: "#/components/parameters/AgentSignatureVersion"
      - $ref: "#/components/parameters/AgentSignedPath"
      - $ref: "#/components/parameters/AgentKindAtlas"
      - $ref: "#/components/parameters/AgentVersion"
      - $ref: "#/components/parameters/AgentIdentityProof"
      - $ref: "#/components/parameters/AtlasSchemaVersion"
    post:
      tags: [ATLAS]
      operationId: ingestAtlasTelemetry
      summary: Ingest an ATLAS schema-v5 snapshot or heartbeat
      description: Full snapshots are strongly typed and replace the current domain state. Heartbeats update liveness without a partial delta merge.
      security: [{ atlasV4Key: [] }]
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/AtlasTelemetryV5" }
      responses: *agentResponses

  /atlas/events:
    parameters: *atlasV4IdentityHeaders
    post:
      tags: [ATLAS]
      operationId: ingestAtlasEvents
      summary: Ingest an ATLAS audit or diagnostic event batch
      security: [{ atlasV4Key: [] }]
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/AgentJsonDocument" }
      responses: *agentResponses

  /atlas/incidents:
    parameters: *atlasV4IdentityHeaders
    post:
      tags: [ATLAS]
      operationId: createAtlasIncident
      summary: Create an incident from an authenticated endpoint signal
      security: [{ atlasV4Key: [] }]
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/AgentJsonDocument" }
      responses: *agentResponses

  /atlas/uninstall-ack:
    parameters: *atlasV4IdentityHeaders
    post:
      tags: [ATLAS]
      operationId: acknowledgeAtlasUninstall
      summary: Confirm that ATLAS removed itself
      security: [{ atlasV4Key: [] }]
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/AgentJsonDocument" }
      responses: *agentResponses

  /atlas/network-observation:
    parameters: *atlasV4IdentityHeaders
    post:
      tags: [ATLAS]
      operationId: ingestAtlasNetworkObservation
      summary: Ingest endpoint-visible ARP, LLDP, mDNS, and IPv6 ND evidence
      security: [{ atlasV4Key: [] }]
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/AgentJsonDocument" }
      responses: *agentResponses

  /atlas/agent-pair:
    parameters: *atlasV4IdentityHeaders
    get:
      tags: [ATLAS]
      operationId: listAtlasAgentPairs
      summary: Find registered same-subnet peers for local cooperation
      security: [{ atlasV4Key: [] }]
      responses: *agentResponses

  /vector/ping:
    parameters: &vectorV4Headers
      - $ref: "#/components/parameters/AgentMachineId"
      - $ref: "#/components/parameters/AgentTimestamp"
      - $ref: "#/components/parameters/AgentNonce"
      - $ref: "#/components/parameters/AgentSignature"
      - $ref: "#/components/parameters/AgentSignatureVersion"
      - $ref: "#/components/parameters/AgentSignedPath"
      - $ref: "#/components/parameters/AgentKindVector"
      - $ref: "#/components/parameters/AgentVersion"
    get:
      tags: [VECTOR]
      operationId: pingVectorAgentApi
      summary: Verify VECTOR connectivity and fleet credentials
      security: [{ vectorV4Key: [] }]
      responses: *agentResponses

  /vector/register:
    parameters: &vectorV4IdentityHeaders
      - $ref: "#/components/parameters/AgentMachineId"
      - $ref: "#/components/parameters/AgentTimestamp"
      - $ref: "#/components/parameters/AgentNonce"
      - $ref: "#/components/parameters/AgentSignature"
      - $ref: "#/components/parameters/AgentSignatureVersion"
      - $ref: "#/components/parameters/AgentSignedPath"
      - $ref: "#/components/parameters/AgentKindVector"
      - $ref: "#/components/parameters/AgentVersion"
      - $ref: "#/components/parameters/AgentIdentityProof"
    post:
      tags: [VECTOR]
      operationId: registerVectorAgent
      summary: Enrol or rotate a VECTOR device identity
      description: First enrolment atomically consumes a 15-minute, single-use capability and binds the submitted Ed25519 and X25519 keys. Rotation requires proof from the currently registered Ed25519 identity and the exact same typed API-key binding.
      security: [{ vectorV4Key: [] }]
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/VectorRegistrationRequest" }
      responses: *agentResponses

  /vector/config:
    parameters: *vectorV4IdentityHeaders
    get:
      tags: [VECTOR]
      operationId: getVectorConfig
      summary: Get the current VECTOR scanner configuration
      security: [{ vectorV4Key: [] }]
      responses: *agentResponses

  /vector/sync-manifest:
    parameters: *vectorV4IdentityHeaders
    get:
      tags: [VECTOR]
      operationId: getVectorSyncManifest
      summary: Get version identifiers for delta-sync resources
      security: [{ vectorV4Key: [] }]
      responses: *agentResponses

  /vector/peers:
    parameters: *vectorV4IdentityHeaders
    get:
      tags: [VECTOR]
      operationId: listVectorInventoryPeers
      summary: Get tenant and customer-scoped network and host inventory
      security: [{ vectorV4Key: [] }]
      responses: *agentResponses

  /vector/vector-peers:
    parameters: *vectorV4IdentityHeaders
    get:
      tags: [VECTOR]
      operationId: listVectorWanPeers
      summary: List active VECTOR instances for WAN path classification
      security: [{ vectorV4Key: [] }]
      responses: *agentResponses

  /vector/peer-link-observations:
    parameters: *vectorV4IdentityHeaders
    post:
      tags: [VECTOR]
      operationId: ingestVectorPeerLinkObservations
      summary: Ingest classified inter-instance link evidence
      security: [{ vectorV4Key: [] }]
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/AgentJsonDocument" }
      responses: *agentResponses

  /vector/agent-pair:
    parameters: *vectorV4IdentityHeaders
    get:
      tags: [VECTOR]
      operationId: listVectorAgentPairs
      summary: Find registered same-subnet peers
      security: [{ vectorV4Key: [] }]
      responses: *agentResponses

  /vector/snmp-credentials:
    parameters:
      - $ref: "#/components/parameters/AgentMachineId"
      - $ref: "#/components/parameters/AgentTimestamp"
      - $ref: "#/components/parameters/AgentNonce"
      - $ref: "#/components/parameters/AgentSignature"
      - $ref: "#/components/parameters/AgentSignatureVersion"
      - $ref: "#/components/parameters/AgentSignedPath"
      - $ref: "#/components/parameters/AgentKindVector"
      - $ref: "#/components/parameters/AgentVersion"
      - $ref: "#/components/parameters/AgentIdentityProof"
      - $ref: "#/components/parameters/TargetIp"
    get:
      tags: [VECTOR]
      operationId: getVectorSnmpCredentials
      summary: Resolve short-lived target-specific SNMP credentials
      description: The signed path includes the target_ip query string byte-for-byte. Responses are signed and must never be persisted by the agent.
      security: [{ vectorV4Key: [] }]
      responses: *agentResponses

  /vector/cdp-observations:
    parameters: *vectorV4IdentityHeaders
    post:
      tags: [VECTOR]
      operationId: ingestVectorCdpObservations
      summary: Ingest Cisco Discovery Protocol observations
      security: [{ vectorV4Key: [] }]
      requestBody: { $ref: "#/components/requestBodies/AgentJson" }
      responses: *agentResponses

  /vector/transceiver-observations:
    parameters: *vectorV4IdentityHeaders
    post:
      tags: [VECTOR]
      operationId: ingestVectorTransceiverObservations
      summary: Ingest optical transceiver inventory and diagnostics
      security: [{ vectorV4Key: [] }]
      requestBody: { $ref: "#/components/requestBodies/AgentJson" }
      responses: *agentResponses

  /vector/fhrp-observations:
    parameters: *vectorV4IdentityHeaders
    post:
      tags: [VECTOR]
      operationId: ingestVectorFhrpObservations
      summary: Ingest VRRP and HSRP redundancy observations
      security: [{ vectorV4Key: [] }]
      requestBody: { $ref: "#/components/requestBodies/AgentJson" }
      responses: *agentResponses

  /vector/bfd-observations:
    parameters: *vectorV4IdentityHeaders
    post:
      tags: [VECTOR]
      operationId: ingestVectorBfdObservations
      summary: Ingest Bidirectional Forwarding Detection sessions
      security: [{ vectorV4Key: [] }]
      requestBody: { $ref: "#/components/requestBodies/AgentJson" }
      responses: *agentResponses

  /vector/cfm-observations:
    parameters: *vectorV4IdentityHeaders
    post:
      tags: [VECTOR]
      operationId: ingestVectorCfmObservations
      summary: Ingest Ethernet Connectivity Fault Management observations
      security: [{ vectorV4Key: [] }]
      requestBody: { $ref: "#/components/requestBodies/AgentJson" }
      responses: *agentResponses

  /vector/scan-ingest:
    parameters: *vectorV4IdentityHeaders
    post:
      tags: [VECTOR]
      operationId: ingestVectorScan
      summary: Publish a complete network discovery result
      description: This is VECTOR's largest write surface and uses a dedicated durable rate-limit bucket before body parsing.
      security: [{ vectorV4Key: [] }]
      requestBody: { $ref: "#/components/requestBodies/AgentJson" }
      responses: *agentResponses

  /integrations/stripe/webhook:
    post:
      tags: [Integrations]
      operationId: receiveStripeWebhook
      summary: Receive a Stripe webhook delivery
      description: The raw request body is verified before JSON processing. Duplicate Stripe event IDs are acknowledged without replaying side effects.
      security: [{ stripeSignature: [] }]
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/ProviderJsonDocument" }
      responses:
        "200":
          description: Event received or already processed.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/StripeWebhookAck" }
        "400": { $ref: "#/components/responses/ProviderError" }
        "413": { $ref: "#/components/responses/ProviderError" }
        "429": { $ref: "#/components/responses/ProviderError" }
        "500": { $ref: "#/components/responses/ProviderError" }

  /integrations/slack/commands:
    parameters:
      - $ref: "#/components/parameters/SlackRequestTimestamp"
    post:
      tags: [Integrations]
      operationId: receiveSlackCommand
      summary: Receive a Slack slash command
      description: Slack requires application-level failures to be represented by an ephemeral 200 response after signature verification.
      security: [{ slackSignature: [] }]
      requestBody:
        required: true
        content:
          application/x-www-form-urlencoded:
            schema: { $ref: "#/components/schemas/SlackCommandRequest" }
      responses:
        "200":
          description: Slack Block Kit or ephemeral command response.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/ProviderJsonDocument" }
        "400": { $ref: "#/components/responses/ProviderError" }
        "401": { $ref: "#/components/responses/ProviderError" }
        "413": { $ref: "#/components/responses/ProviderError" }
        "429": { $ref: "#/components/responses/ProviderError" }
        "500": { $ref: "#/components/responses/ProviderError" }

  /integrations/slack/interactions:
    parameters:
      - $ref: "#/components/parameters/SlackRequestTimestamp"
    post:
      tags: [Integrations]
      operationId: receiveSlackInteraction
      summary: Receive a Slack Block Kit interaction
      security: [{ slackSignature: [] }]
      requestBody:
        required: true
        content:
          application/x-www-form-urlencoded:
            schema:
              type: object
              additionalProperties: false
              required: [payload]
              properties:
                payload:
                  type: string
                  description: JSON-encoded Slack interaction payload.
      responses:
        "200":
          description: Interaction accepted.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/ProviderJsonDocument" }
        "400": { $ref: "#/components/responses/ProviderError" }
        "401": { $ref: "#/components/responses/ProviderError" }
        "413": { $ref: "#/components/responses/ProviderError" }
        "429": { $ref: "#/components/responses/ProviderError" }
        "500": { $ref: "#/components/responses/ProviderError" }

  /integrations/teams/activities:
    post:
      tags: [Integrations]
      operationId: receiveTeamsActivity
      summary: Receive a Microsoft Bot Framework activity
      description: The bearer JWT signature, issuer, and audience are verified against Microsoft's published keys before the body is processed.
      security: [{ teamsBearer: [] }]
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/TeamsActivity" }
      responses:
        "200":
          description: Bot Framework activity response.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/ProviderJsonDocument" }
        "400": { $ref: "#/components/responses/ProviderError" }
        "401": { $ref: "#/components/responses/ProviderError" }
        "413": { $ref: "#/components/responses/ProviderError" }
        "429": { $ref: "#/components/responses/ProviderError" }
        "500": { $ref: "#/components/responses/ProviderError" }

  /calendar/feeds/{token}:
    get:
      tags: [Calendar]
      operationId: getCalendarFeed
      summary: Retrieve an iCalendar subscription feed
      description: The 256-bit token in the path is the credential. Keep the complete URL out of logs, analytics, screenshots, and support messages.
      security: []
      parameters:
        - $ref: "#/components/parameters/CalendarFeedToken"
      responses:
        "200":
          description: iCalendar feed. A transient data error yields a valid empty calendar so subscribed clients keep polling.
          headers:
            Content-Disposition:
              description: Suggested `.ics` filename.
              schema: { type: string }
          content:
            text/calendar:
              schema: { type: string }
        "404":
          description: Token is invalid, expired, revoked, or unknown. The response intentionally does not distinguish these states.
          content:
            text/plain:
              schema: { type: string, const: Not found }
        "429":
          description: Per-IP feed polling limit exceeded.

components:
  securitySchemes:
    publicApiKey:
      type: http
      scheme: bearer
      bearerFormat: kvr_live_*
      description: Tenant-bound public API key from Workspace Settings > API Keys.
    scimBearer:
      type: http
      scheme: bearer
      bearerFormat: Kovira SCIM token
      description: Dedicated, tenant-bound provisioning token issued in Workspace Settings > SCIM.
    atlasV4Key:
      type: apiKey
      in: header
      name: X-API-Key
      description: ATLAS-only workspace agent secret in canonical `kovira_` plus 64 lowercase hex format. The stored key kind and exact ATLAS device registration must match the route and signed headers.
    vectorV4Key:
      type: apiKey
      in: header
      name: X-API-Key
      description: VECTOR-only workspace agent secret in canonical `kovira_` plus 64 lowercase hex format. The stored key kind and exact VECTOR device registration must match the route and signed headers.
    stripeSignature:
      type: apiKey
      in: header
      name: Stripe-Signature
      description: Stripe's signature over the unmodified request body.
    slackSignature:
      type: apiKey
      in: header
      name: X-Slack-Signature
      description: Slack v0 HMAC signature over the timestamp and unmodified form body.
    teamsBearer:
      type: http
      scheme: bearer
      bearerFormat: Microsoft Bot Framework JWT
      description: Microsoft-issued Bot Framework JWT with verified signature, issuer, and Kovira bot audience.

  headers:
    ApiVersion:
      description: Major contract version that produced this response.
      schema: { type: string, const: "1" }
    RateLimitLimit:
      description: Requests allowed in the current window.
      schema: { type: integer, example: 60 }
    RateLimitRemaining:
      description: Requests remaining in the current window.
      schema: { type: integer, minimum: 0 }
    RateLimitReset:
      description: Seconds until the current window resets.
      schema: { type: integer, minimum: 1 }
    RequestId:
      description: UUID correlation identifier echoed from the request or generated by Kovira.
      schema: { type: string, format: uuid }
    ResponseSignatureVersion:
      description: Agent response-signing protocol version.
      schema: { type: string, const: "4" }
    ResponseSignature:
      description: HMAC-SHA256 signature over the wire-v4 response canonical message.
      schema: { type: string, pattern: "^[a-f0-9]{64}$" }

  parameters:
    ResourceId:
      in: path
      name: id
      required: true
      schema: { type: string, format: uuid }
    Cursor:
      in: query
      name: cursor
      description: Opaque cursor returned in `meta.next_cursor`. Do not construct or decode it.
      schema: { type: string, maxLength: 512 }
    Limit100:
      in: query
      name: limit
      schema: { type: integer, minimum: 1, maximum: 100, default: 25 }
    Limit200:
      in: query
      name: limit
      schema: { type: integer, minimum: 1, maximum: 200, default: 25 }
    ScimFilter:
      in: query
      name: filter
      description: Restricted RFC 7644 equality filter. User fields are `userName`, `emails.value`, `externalId`, and `active`; group fields are `displayName` and `externalId`.
      schema: { type: string, maxLength: 512 }
    ScimStartIndex:
      in: query
      name: startIndex
      description: RFC 7644 one-based result index.
      schema: { type: integer, minimum: 1, default: 1 }
    ScimCount:
      in: query
      name: count
      schema: { type: integer, minimum: 0, maximum: 200, default: 100 }
    AgentMachineId:
      in: header
      name: X-Machine-Id
      required: true
      description: Stable device identifier bound into the wire-v4 canonical message.
      schema: { type: string, format: uuid }
    AgentTimestamp:
      in: header
      name: X-Timestamp
      required: true
      description: Unix epoch seconds. The server accepts a narrow clock-skew window.
      schema: { type: string, pattern: "^[0-9]{10}$" }
    AgentNonce:
      in: header
      name: X-Nonce
      required: true
      description: Single-use, replay-protected 16-byte nonce encoded as lowercase hexadecimal.
      schema: { type: string, pattern: "^[a-f0-9]{32}$" }
    AgentSignature:
      in: header
      name: X-Signature
      required: true
      description: Lowercase hexadecimal HMAC-SHA256 of the wire-v4 canonical request.
      schema: { type: string, pattern: "^[a-f0-9]{64}$" }
    AgentSignatureVersion:
      in: header
      name: X-Sig-Version
      required: true
      schema: { type: string, const: "4" }
    AgentSignedPath:
      in: header
      name: X-Signed-Path
      required: true
      description: Exact request path and query string, byte-for-byte. It must match the URL target.
      schema:
        { type: string, pattern: "^/v1/(atlas|vector)/[a-z0-9-]+(?:\\?.*)?$" }
    AgentKindAtlas:
      in: header
      name: X-Agent-Kind
      required: true
      schema: { type: string, const: atlas }
    AgentKindVector:
      in: header
      name: X-Agent-Kind
      required: true
      schema: { type: string, const: vector }
    AgentVersion:
      in: header
      name: X-Agent-Version
      required: true
      description: Semantic version of the calling agent. Versions below the fleet minimum receive 426 Upgrade Required.
      schema:
        { type: string, pattern: "^\\d+\\.\\d+\\.\\d+(?:-[0-9A-Za-z.-]+)?$" }
    AgentIdentityProof:
      in: header
      name: X-Identity-Proof
      required: true
      description: Padded base64 Ed25519 signature of the same wire-v4 canonical request using the registered device private key.
      schema: { type: string, minLength: 88, maxLength: 88 }
    AtlasSchemaVersion:
      in: header
      name: X-Schema-Version
      required: true
      schema: { type: string, const: "5" }
    TargetIp:
      in: query
      name: target_ip
      required: true
      description: Target address whose tenant-authorised SNMP credentials are required. This query parameter is part of X-Signed-Path.
      schema: { type: string, format: ip }
    SlackRequestTimestamp:
      in: header
      name: X-Slack-Request-Timestamp
      required: true
      description: Unix seconds used in the Slack v0 signature and checked against a five-minute replay window.
      schema: { type: integer, format: int64 }
    CalendarFeedToken:
      in: path
      name: token
      required: true
      description: 256-bit random bearer credential encoded as 64 lowercase hexadecimal characters.
      schema: { type: string, pattern: "^[a-f0-9]{64}$" }

  requestBodies:
    AgentJson:
      required: true
      content:
        application/json:
          schema: { $ref: "#/components/schemas/AgentJsonDocument" }

  schemas:
    GraphqlRequest:
      type: object
      additionalProperties: false
      required: [query]
      properties:
        query: { type: string, minLength: 1 }
        variables:
          type: [object, "null"]
          additionalProperties: true
        operationName: { type: [string, "null"] }
    GraphqlResponse:
      type: object
      properties:
        data: { type: [object, "null"], additionalProperties: true }
        errors:
          type: array
          items:
            type: object
            required: [message]
            properties:
              message: { type: string }
              path: { type: array, items: { type: [string, integer] } }
              extensions: { type: object, additionalProperties: true }
      anyOf:
        - required: [data]
        - required: [errors]
    ScimMeta:
      type: object
      properties:
        resourceType: { type: string }
        created: { type: string, format: date-time }
        lastModified: { type: string, format: date-time }
        location: { type: string, format: uri }
    ScimEmail:
      type: object
      required: [value]
      properties:
        value: { type: string, format: email }
        type: { type: string, default: work }
        primary: { type: boolean }
    ScimMember:
      type: object
      required: [value]
      properties:
        value: { type: string, format: uuid }
        display: { type: string }
        $ref: { type: string, format: uri-reference }
    ScimUserWrite:
      type: object
      required: [userName]
      properties:
        schemas: { type: array, items: { type: string } }
        userName: { type: string, minLength: 1, maxLength: 320 }
        displayName: { type: string, maxLength: 200 }
        name:
          type: object
          properties:
            givenName: { type: string }
            familyName: { type: string }
            formatted: { type: string }
        emails:
          { type: array, items: { $ref: "#/components/schemas/ScimEmail" } }
        externalId: { type: [string, "null"], maxLength: 256 }
        active: { type: boolean, default: true }
        title: { type: [string, "null"], maxLength: 200 }
    ScimUser:
      allOf:
        - $ref: "#/components/schemas/ScimUserWrite"
        - type: object
          required: [id, schemas, meta]
          properties:
            id: { type: string, format: uuid, readOnly: true }
            schemas:
              type: array
              items: { type: string }
              readOnly: true
            meta: { $ref: "#/components/schemas/ScimMeta" }
    ScimGroupWrite:
      type: object
      required: [displayName]
      properties:
        schemas: { type: array, items: { type: string } }
        displayName: { type: string, minLength: 1, maxLength: 100 }
        externalId: { type: [string, "null"], maxLength: 256 }
        members:
          type: array
          maxItems: 500
          items: { $ref: "#/components/schemas/ScimMember" }
    ScimGroup:
      allOf:
        - $ref: "#/components/schemas/ScimGroupWrite"
        - type: object
          required: [id, schemas, meta]
          properties:
            id: { type: string, format: uuid, readOnly: true }
            schemas:
              type: array
              items: { type: string }
              readOnly: true
            meta: { $ref: "#/components/schemas/ScimMeta" }
    ScimListResponse:
      type: object
      required: [schemas, totalResults, startIndex, itemsPerPage, Resources]
      properties:
        schemas:
          type: array
          items: { type: string }
        totalResults: { type: integer, minimum: 0 }
        startIndex: { type: integer, minimum: 1 }
        itemsPerPage: { type: integer, minimum: 0 }
        Resources: { type: array, items: {} }
    ScimPatchRequest:
      type: object
      additionalProperties: false
      required: [schemas, Operations]
      properties:
        schemas:
          type: array
          contains: { const: "urn:ietf:params:scim:api:messages:2.0:PatchOp" }
        Operations:
          type: array
          minItems: 1
          maxItems: 100
          items:
            type: object
            additionalProperties: false
            required: [op]
            properties:
              op: { type: string, enum: [add, replace, remove] }
              path: { type: string }
              value: {}
    ScimErrorBody:
      type: object
      additionalProperties: false
      required: [schemas, status, detail]
      properties:
        schemas:
          type: array
          items: { type: string }
        status: { type: string, pattern: "^[45]\\d\\d$" }
        scimType: { type: string }
        detail: { type: string }
    AgentJsonDocument:
      type: object
      additionalProperties: true
      description: Endpoint-specific agent JSON document. Agent releases and server handlers are updated together; unknown top-level fields are ignored only where the endpoint contract explicitly permits them.
    AtlasRegistrationRequest:
      type: object
      additionalProperties: false
      required: [machine_id, ed25519_pubkey]
      description: ATLAS device identity. `enrollment_token` is required for first enrolment and optional for a current-identity rotation retry. Private keys are never transmitted.
      properties:
        machine_id: { type: string, format: uuid }
        ed25519_pubkey:
          {
            type: string,
            pattern: "^[A-Za-z0-9+/]{43}=$",
            description: Non-zero 32-byte Ed25519 public key in padded base64.,
          }
        enrollment_token:
          {
            type: string,
            pattern: "^kovira_enroll_[0-9a-f]{64}$",
            writeOnly: true,
          }
        agent_version: { type: string, maxLength: 64 }
        hostname: { type: string, maxLength: 255 }
        os: { type: string, maxLength: 128 }
    VectorRegistrationRequest:
      type: object
      additionalProperties: false
      required: [instance_id, ed25519_pubkey, x25519_pubkey]
      description: VECTOR scanner identity. `enrollment_token` is required for first enrolment and optional for a current-identity rotation retry. Private keys are never transmitted.
      properties:
        instance_id: { type: string, format: uuid }
        ed25519_pubkey:
          {
            type: string,
            pattern: "^[A-Za-z0-9+/]{43}=$",
            description: Non-zero 32-byte Ed25519 public key in padded base64.,
          }
        x25519_pubkey:
          {
            type: string,
            pattern: "^[A-Za-z0-9+/]{43}=$",
            description: Non-zero 32-byte X25519 public key in padded base64.,
          }
        enrollment_token:
          {
            type: string,
            pattern: "^kovira_enroll_[0-9a-f]{64}$",
            writeOnly: true,
          }
        hostname: { type: [string, "null"], maxLength: 255 }
        agent_version: { type: [string, "null"], maxLength: 64 }
        os: { type: [string, "null"], maxLength: 128 }
        scanned_cidrs:
          { type: [array, "null"], maxItems: 256, items: { type: string } }
        site_label: { type: [string, "null"], maxLength: 255 }
    AtlasTelemetryV5:
      type: object
      additionalProperties: true
      required:
        [
          schema_version,
          payload_type,
          machine_id,
          agent_id,
          agent_version,
          tenant_id,
          collected_at,
          partial,
          domains,
        ]
      properties:
        schema_version: { type: integer, const: 5 }
        payload_type: { type: string, enum: [full, heartbeat] }
        machine_id: { type: string, format: uuid }
        agent_id: { type: string, format: uuid }
        agent_version: { type: string }
        tenant_id: { type: string, format: uuid }
        collected_at: { type: string, format: date-time }
        partial: { type: boolean }
        domains:
          type: array
          uniqueItems: true
          items:
            {
              type: string,
              enum:
                [
                  identity,
                  os,
                  hardware,
                  network,
                  storage,
                  security,
                  software,
                  telemetry,
                ],
            }
        identity: { type: object, additionalProperties: true }
        os: { type: object, additionalProperties: true }
        hardware: { type: object, additionalProperties: true }
        network: { type: object, additionalProperties: true }
        storage: { type: object, additionalProperties: true }
        security: { type: object, additionalProperties: true }
        software: { type: object, additionalProperties: true }
        telemetry: { type: object, additionalProperties: true }
    AgentResponse:
      type: object
      additionalProperties: true
      description: Endpoint-specific successful agent response. Successful operational responses are wire-v4 signed.
    ProviderJsonDocument:
      type: object
      additionalProperties: true
    StripeWebhookAck:
      type: object
      additionalProperties: false
      required: [received]
      properties:
        received: { type: boolean, const: true }
        duplicate: { type: boolean }
    SlackCommandRequest:
      type: object
      additionalProperties: true
      required: [team_id, user_id, command]
      properties:
        team_id: { type: string }
        user_id: { type: string }
        command: { type: string, const: /kovira }
        text: { type: string }
        response_url: { type: string, format: uri, writeOnly: true }
        trigger_id: { type: string }
    TeamsActivity:
      type: object
      additionalProperties: true
      required: [type]
      properties:
        type: { type: string, enum: [message, invoke] }
        id: { type: string }
        text: { type: string }
        serviceUrl: { type: string, format: uri }
        conversation: { type: object, additionalProperties: true }
        channelData: { type: object, additionalProperties: true }
        from: { type: object, additionalProperties: true }
        recipient: { type: object, additionalProperties: true }
        value: { type: object, additionalProperties: true }
    HealthEnvelope:
      type: object
      additionalProperties: false
      required: [status, service, version, checked_at]
      properties:
        status: { type: string, const: ok }
        service: { type: string, const: kovira-public-api }
        version: { type: string, const: "1" }
        checked_at: { type: string, format: date-time }
    DiscoveryEnvelope:
      type: object
      required:
        [
          name,
          version,
          status,
          base_url,
          groups,
          authentication,
          documentation,
          openapi,
          graphql,
        ]
      properties:
        name: { type: string, const: Kovira Public API }
        version: { type: string, const: "1" }
        status: { type: string, const: available }
        base_url: { type: string, format: uri }
        groups:
          type: array
          items: { type: string }
        authentication:
          type: object
          required: [customer_api, atlas, vector, scim, integrations, calendar]
          additionalProperties: false
          properties:
            customer_api: { type: string }
            atlas: { type: string }
            vector: { type: string }
            scim: { type: string }
            integrations: { type: string }
            calendar: { type: string }
        documentation: { type: string, format: uri }
        openapi: { type: string, format: uri }
        graphql: { type: string, format: uri }
    Pagination:
      type: object
      required: [next_cursor]
      properties:
        next_cursor:
          oneOf:
            - type: string
            - type: "null"
          description: Opaque cursor, or null when there is no next page.
    PageEnvelope:
      type: object
      required: [data, meta]
      properties:
        data: { type: array, items: {} }
        meta: { $ref: "#/components/schemas/Pagination" }
    Error:
      type: object
      required: [error]
      properties:
        error:
          type: object
          required: [code, message]
          properties:
            code: { type: string, example: validation_failed }
            message: { type: string }
    IncidentStatus:
      type: string
      enum: [new, assigned, in_progress, closed]
    Incident:
      type: object
      required: [id, number, title, status, created_at, updated_at]
      properties:
        id: { type: string, format: uuid }
        number: { type: string }
        title: { type: string }
        description: { type: [string, "null"] }
        status: { $ref: "#/components/schemas/IncidentStatus" }
        priority: { type: string, enum: [p1, p2, p3, p4] }
        impact: { type: string, enum: [high, medium, low] }
        urgency: { type: string, enum: [high, medium, low] }
        category:
          {
            type: [string, "null"],
            enum: [outage, performance, access, security, data, other, null],
          }
        source:
          { type: string, enum: [user_reported, monitoring, internal, vendor] }
        assignee_id: { type: [string, "null"], format: uuid }
        customer_id: { type: [string, "null"], format: uuid }
        created_at: { type: string, format: date-time }
        updated_at: { type: string, format: date-time }
        resolved_at: { type: [string, "null"], format: date-time }
        closed_at: { type: [string, "null"], format: date-time }
        resolution_summary: { type: [string, "null"] }
        tags: { type: array, maxItems: 20, items: { type: string } }
    IncidentCreate:
      type: object
      additionalProperties: false
      required: [title]
      properties:
        title: { type: string, minLength: 1, maxLength: 255 }
        description: { type: [string, "null"] }
        impact: { type: string, enum: [high, medium, low], default: medium }
        urgency: { type: string, enum: [high, medium, low], default: medium }
        category:
          {
            type: [string, "null"],
            enum: [outage, performance, access, security, data, other, null],
          }
        source:
          {
            type: string,
            enum: [user_reported, monitoring, internal, vendor],
            default: user_reported,
          }
        assignee_id: { type: [string, "null"], format: uuid }
        is_major: { type: boolean, default: false }
        tags:
          { type: array, maxItems: 20, items: { type: string, maxLength: 50 } }
    IncidentUpdate:
      type: object
      additionalProperties: false
      minProperties: 1
      properties:
        title: { type: string, minLength: 1, maxLength: 255 }
        description: { type: [string, "null"] }
        impact: { type: string, enum: [high, medium, low] }
        urgency: { type: string, enum: [high, medium, low] }
        category:
          {
            type: [string, "null"],
            enum: [outage, performance, access, security, data, other, null],
          }
        source:
          { type: string, enum: [user_reported, monitoring, internal, vendor] }
        assignee_id: { type: [string, "null"], format: uuid }
        resolution_summary: { type: [string, "null"] }
        resolution_details: { type: [string, "null"] }
        is_major: { type: boolean }
        tags:
          { type: array, maxItems: 20, items: { type: string, maxLength: 50 } }
    IncidentTransition:
      type: object
      additionalProperties: false
      required: [status]
      properties:
        status: { $ref: "#/components/schemas/IncidentStatus" }
        resolution_summary: { type: string, minLength: 1 }
        resolution_details: { type: [string, "null"] }
        resolution_code:
          type: string
          enum:
            [
              permanent_fix,
              workaround,
              configuration_change,
              restart_recovery,
              hardware_replacement,
              vendor_patch,
              user_education,
              no_fault_found,
              duplicate,
              not_an_incident,
            ]
        closure_code:
          type: string
          enum:
            [
              resolved,
              resolved_with_workaround,
              cancelled,
              merged,
              out_of_scope,
              auto_resolved,
            ]
        postmortem_document_id: { type: [string, "null"], format: uuid }
        reopen_reason: { type: string, minLength: 1 }
    IncidentMutationEnvelope:
      type: object
      required: [data]
      properties:
        data:
          type: object
          required: [id, number]
          properties:
            id: { type: string, format: uuid }
            number: { type: string }
    CIStatus:
      type: string
      enum: [active, inactive, archived, maintenance]
    CIType:
      type: string
      enum:
        [
          device,
          network,
          service,
          rack,
          domain,
          document,
          vendor,
          location,
          contract,
          person,
          group,
          incident,
          change,
          password,
          api,
          license,
          software,
          wan,
          sla,
          entra_id,
          intune,
          saas_app,
          business_service,
          quote,
        ]
    WritableCIType:
      type: string
      enum:
        [
          device,
          network,
          service,
          rack,
          domain,
          document,
          vendor,
          location,
          contract,
          person,
          group,
          api,
          license,
          software,
          wan,
          sla,
          saas_app,
          business_service,
          quote,
        ]
    CI:
      type: object
      required: [id, type, name]
      additionalProperties: true
      properties:
        id: { type: string, format: uuid }
        type: { $ref: "#/components/schemas/CIType" }
        name: { type: string }
        description: { type: [string, "null"] }
        status: { $ref: "#/components/schemas/CIStatus" }
        customer_id: { type: [string, "null"], format: uuid }
        created_at: { type: string, format: date-time }
        updated_at: { type: string, format: date-time }
    CICreate:
      type: object
      additionalProperties: false
      required: [type, name]
      properties:
        type: { $ref: "#/components/schemas/WritableCIType" }
        name: { type: string, minLength: 1, maxLength: 255 }
        description: { type: [string, "null"], maxLength: 2000 }
        status: { $ref: "#/components/schemas/CIStatus" }
        customer_id: { type: [string, "null"], format: uuid }
    CIUpdate:
      type: object
      additionalProperties: false
      minProperties: 1
      properties:
        name: { type: string, minLength: 1, maxLength: 255 }
        description: { type: [string, "null"], maxLength: 2000 }
        status: { $ref: "#/components/schemas/CIStatus" }
        customer_id: { type: [string, "null"], format: uuid }
    CIMutationEnvelope:
      type: object
      required: [data]
      properties:
        data:
          type: object
          required: [id, type, name]
          properties:
            id: { type: string, format: uuid }
            type: { type: string }
            name: { type: string }
    Transceiver:
      type: object
      required:
        [
          id,
          device_ci_id,
          port_name,
          form_factor,
          vendor_name,
          serial_number,
          media_type,
          observed_at,
        ]
      properties:
        id: { type: string, format: uuid }
        device_ci_id: { type: string, format: uuid }
        port_name: { type: string }
        form_factor: { type: string }
        vendor_name: { type: string }
        vendor_part: { type: [string, "null"] }
        serial_number: { type: string }
        media_type: { type: string }
        wavelength_nm: { type: [integer, "null"] }
        reach_meters: { type: [integer, "null"] }
        is_vendor_compatible: { type: [boolean, "null"] }
        observed_at: { type: string, format: date-time }
        ddm_lane: { type: [integer, "null"], minimum: 0, maximum: 7 }
        ddm_temperature_c: { type: [number, "null"] }
        ddm_voltage_v: { type: [number, "null"] }
        ddm_tx_bias_ma: { type: [number, "null"] }
        ddm_tx_power_dbm: { type: [number, "null"] }
        ddm_rx_power_dbm: { type: [number, "null"] }
        ddm_alarm_flags: { type: [integer, "null"] }
        ddm_observed_at: { type: [string, "null"], format: date-time }
    NetworkAnomaly:
      type: object
      required:
        [
          id,
          anomaly_type,
          severity,
          title,
          fingerprint,
          first_seen_at,
          last_seen_at,
          acknowledged,
        ]
      properties:
        id: { type: string, format: uuid }
        anomaly_type: { $ref: "#/components/schemas/NetworkAnomalyType" }
        severity: { type: string, enum: [critical, warning, info] }
        ci_id: { type: [string, "null"], format: uuid }
        title: { type: string }
        detail: { type: [object, array, string, number, boolean, "null"] }
        fingerprint: { type: string }
        first_seen_at: { type: string, format: date-time }
        last_seen_at: { type: string, format: date-time }
        resolved_at: { type: [string, "null"], format: date-time }
        acknowledged: { type: boolean }
    NetworkAnomalyType:
      type: string
      enum:
        [
          duplicate_mac_nontrunk,
          stp_root_flap,
          port_flap,
          asymmetric_path,
          vlan_leak,
          mac_move,
          ecmp_confusion,
          mtu_mismatch,
          duplex_mismatch,
          rogue_dhcp,
          native_vlan_mismatch,
          broadcast_storm,
          oversubscribed_uplink,
          unreachable_peer,
          cfm_mep_loc,
          synce_quality_degraded,
          prp_wrong_lan_burst,
          hsr_loss_burst,
          profinet_name_conflict,
          eth_oam_link_fault,
          lldp_cdp_neighbor_divergence,
          vrrp_split_brain,
          hsrp_split_brain,
          bfd_session_down,
          transceiver_low_rx_power,
          transceiver_high_temp,
          transceiver_alarm,
        ]
    CdpNeighbour:
      type: object
      required:
        [
          id,
          local_switch_ci_id,
          local_port_name,
          neighbor_device_id,
          observed_at,
          last_seen_at,
        ]
      properties:
        id: { type: string, format: uuid }
        local_switch_ci_id: { type: string, format: uuid }
        local_port_name: { type: string }
        neighbor_device_id: { type: string }
        neighbor_port_id: { type: [string, "null"] }
        capabilities: { type: [array, object, string, "null"] }
        software_version: { type: [string, "null"] }
        platform: { type: [string, "null"] }
        native_vlan: { type: [integer, "null"] }
        duplex: { type: [string, "null"] }
        management_address: { type: [string, "null"] }
        observed_at: { type: string, format: date-time }
        last_seen_at: { type: string, format: date-time }

  responses:
    GraphqlResult:
      description: GraphQL execution result. GraphQL validation and resolver failures are represented in the `errors` array.
      headers:
        Kovira-API-Version: { $ref: "#/components/headers/ApiVersion" }
      content:
        application/graphql-response+json:
          schema: { $ref: "#/components/schemas/GraphqlResponse" }
        application/json:
          schema: { $ref: "#/components/schemas/GraphqlResponse" }
    ScimDocument:
      description: SCIM discovery document.
      content:
        application/scim+json:
          schema:
            oneOf:
              - type: object
                additionalProperties: true
              - type: array
                items: { type: object, additionalProperties: true }
    ScimError:
      description: RFC 7644 error response.
      content:
        application/scim+json:
          schema: { $ref: "#/components/schemas/ScimErrorBody" }
    AgentJson:
      description: Successful agent response.
      headers:
        Kovira-API-Version: { $ref: "#/components/headers/ApiVersion" }
        X-Response-Sig-Version:
          $ref: "#/components/headers/ResponseSignatureVersion"
        X-Response-Signature:
          $ref: "#/components/headers/ResponseSignature"
      content:
        application/json:
          schema: { $ref: "#/components/schemas/AgentResponse" }
    AgentError:
      description: Agent request rejected. Common statuses are 400, 401, 403, 404, 409, 413, 426, and 429.
      headers:
        Kovira-API-Version: { $ref: "#/components/headers/ApiVersion" }
        Retry-After:
          description: Seconds before retrying when the request was rate-limited.
          schema: { type: integer, minimum: 1 }
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
    ProviderError:
      description: Provider request rejected or callback processing failed.
      content:
        application/json:
          schema:
            type: object
            additionalProperties: true
            properties:
              error: { type: [string, object] }
        text/plain:
          schema: { type: string }
    IncidentList:
      description: Cursor-paginated incidents.
      headers:
        Kovira-API-Version: { $ref: "#/components/headers/ApiVersion" }
      content:
        application/json:
          schema:
            allOf:
              - $ref: "#/components/schemas/PageEnvelope"
              - type: object
                properties:
                  data:
                    type: array
                    items: { $ref: "#/components/schemas/Incident" }
    CIList:
      description: Cursor-paginated CIs.
      headers:
        Kovira-API-Version: { $ref: "#/components/headers/ApiVersion" }
      content:
        application/json:
          schema:
            allOf:
              - $ref: "#/components/schemas/PageEnvelope"
              - type: object
                properties:
                  data:
                    type: array
                    items: { $ref: "#/components/schemas/CI" }
    BadRequest:
      description: Invalid path, query, cursor, JSON, or request fields.
      headers:
        Kovira-API-Version: { $ref: "#/components/headers/ApiVersion" }
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
    Unauthorized:
      description: Missing, malformed, expired, or revoked public API key.
      headers:
        Kovira-API-Version: { $ref: "#/components/headers/ApiVersion" }
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
    Forbidden:
      description: Key lacks the required scope or its workspace is unavailable.
      headers:
        Kovira-API-Version: { $ref: "#/components/headers/ApiVersion" }
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
    NotFound:
      description: Resource not found in the authenticated workspace.
      headers:
        Kovira-API-Version: { $ref: "#/components/headers/ApiVersion" }
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
    Conflict:
      description: State transition or resource mutability conflict.
      headers:
        Kovira-API-Version: { $ref: "#/components/headers/ApiVersion" }
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
    PayloadTooLarge:
      description: Request body exceeds 1 MiB.
      headers:
        Kovira-API-Version: { $ref: "#/components/headers/ApiVersion" }
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
    RateLimited:
      description: Rate limit exceeded.
      headers:
        Kovira-API-Version: { $ref: "#/components/headers/ApiVersion" }
        X-RateLimit-Limit: { $ref: "#/components/headers/RateLimitLimit" }
        X-RateLimit-Remaining:
          { $ref: "#/components/headers/RateLimitRemaining" }
        X-RateLimit-Reset: { $ref: "#/components/headers/RateLimitReset" }
        Retry-After:
          description: Seconds before retrying.
          schema: { type: integer, minimum: 1 }
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
    InternalError:
      description: Unexpected server error.
      headers:
        Kovira-API-Version: { $ref: "#/components/headers/ApiVersion" }
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
    Unavailable:
      description: A required safety dependency is temporarily unavailable.
      headers:
        Kovira-API-Version: { $ref: "#/components/headers/ApiVersion" }
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
    MethodNotAllowed:
      description: The path exists but does not accept this method.
      headers:
        Allow:
          description: Methods accepted by this path.
          schema: { type: string }
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
