openapi: 3.1.0
info:
  title: Bairav Ingest API
  version: 1.0.1
  summary: Machine-facing endpoints for sensors, security-event exporters, OpenTelemetry and CI.
  description: |
    The Ingest API accepts telemetry from software you run. It uses long-lived **machine tokens**, not user sessions.

    | Token | Prefix | Header | Used for |
    |---|---|---|---|
    | Sensor enrolment token | `bst_` | `X-Sensor-Token` | Sensor events and the governance channel |
    | Ingest token | `obt_` | `X-Ingest-Token` (or `Authorization: Bearer`) | Security events, OpenTelemetry traces, SBOMs |

    Tokens are shown once when you create them and stored only as an Argon2id hash. Revoke a token to stop it working.

    **Common behaviour**

    - Every request is checked in this order: declared size, token, body size, schema, rate limit, then storage.
    - Ingest is **idempotent**: re-sending an event ID you already sent reports it as a duplicate and changes nothing.
    - Validation problems reject the single event, not the whole batch. The response lists each rejected event with a reason.
    - Rate limits fail **closed**: if the limiter is unavailable you get `503` with `Retry-After`, never unlimited traffic.
    - Prompts, responses and tool arguments are masked at ingest.
  contact:
    name: Bairav developer support
    email: BTG.Support@prokraya.com
  x-source:
    repo: Bairav-MVP2
    commit: 870b92fe954c5ecc55c3ae745e2c6f1f8cf43dd1
    exportedAt: 2026-10-29T17:08:05Z
servers:
  - url: https://{host}
    description: Your Bairav deployment
    variables:
      host:
        default: your-tenant.example.com
        description: The hostname of your Bairav deployment.
tags:
  - name: Sensor
    description: Endpoint sensor telemetry and the reverse governance channel.
  - name: Security events
    description: Normalised security events from your own tools.
  - name: OpenTelemetry
    description: OTLP/HTTP trace receiver for LLM and tool-call spans.
  - name: Inventory
    description: Software bills of materials pushed from CI.
  - name: Health
    description: Liveness.
security: []
paths:
  /api/ingest/events:
    post:
      tags:
        - Sensor
      operationId: ingestSensorEvents
      summary: Send sensor events
      description: |
        Send a batch of events collected on a host. At most **500 events** and **256 KB** per request, and **1,200 events per minute per token**.

        Each event needs `payload.schema_version` set to `"1.0"` (`agent.config` events also accept `"1.1"`). Events whose `collected_at` is outside the accepted time window are rejected individually.
      security:
        - SensorToken: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/SensorBatch"
            example:
              device_key: build-agent-04
              events:
                - event_id: 7d9f0c52-1c6e-4a4e-8a53-2c1c6f3d9a01
                  type: heartbeat
                  collected_at: 2026-10-29T09:12:44Z
                  payload:
                    schema_version: "1.0"
      responses:
        "202":
          description: Accepted. Check `rejected` for events that were refused individually.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/SensorIngestResult"
              example:
                accepted: 1
                duplicates: 0
                rejected: []
                late: 0
        "401":
          $ref: "#/components/responses/Unauthorized"
        "413":
          $ref: "#/components/responses/TooLarge"
        "422":
          $ref: "#/components/responses/Invalid"
        "429":
          $ref: "#/components/responses/RateLimited"
        "503":
          $ref: "#/components/responses/Unavailable"
  /api/ingest/governance:
    get:
      tags:
        - Sensor
      operationId: getGovernanceDirectives
      summary: Poll governance directives
      description: |
        The reverse control channel. A sensor polls this each cycle and enforces the result on the host. An agent is `active` unless a reviewer has suspended, blocked or rejected it, so lifting a hold in Bairav re-activates the agent on the next poll.

        Guardrail policies attached to an agent are included, so attaching a guardrail in Bairav starts enforcing it on the host.
      security:
        - SensorToken: []
      parameters:
        - name: device_key
          in: query
          required: true
          description: The same `device_key` the sensor uses when it sends events.
          schema:
            type: string
      responses:
        "200":
          description: The current governance state of every monitored entity on the device.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/GovernanceDirectives"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "429":
          $ref: "#/components/responses/RateLimited"
  /api/ingest/security-events:
    post:
      tags:
        - Security events
      operationId: ingestSecurityEvents
      summary: Send security events
      description: |
        Send normalised security events from your own tooling (EDR exports, an API gateway, an MCP gateway). At most **500 events** and **256 KB** per request, and **600 events per minute per token**.

        The token is bound to a telemetry source when you create it (`mcp_gateway`, `api_gateway`, `otel`, `imported`, `infrastructure`, `sensor` or `test_fixture`). Events are idempotent on `(tenant, source, event_uid)`.

        Events that fail validation are returned in `rejected` with a reason and are **not** stored. Events that validated but failed processing are parked in a dead-letter queue and retried automatically; `dead_lettered` counts them.
      security:
        - IngestToken: []
        - IngestBearer: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/SecurityEventBatch"
            example:
              schema_version: "1.0"
              events:
                - event_uid: edr-20261008-000123
                  occurred_at: 2026-10-08T09:12:44Z
                  event_type: llm.call
                  severity: info
                  observed_identity: host:BTG-LAP-04/proc:python
                  trace_id: 4bf92f3577b34da6a3ce929d0e0e4736
                  summary: POST api.openai.com /v1/chat/completions
                  fields:
                    host: api.openai.com
                    port: 443
                    sdk: openai-python/1.40
      responses:
        "202":
          description: Accepted.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/SecurityIngestResult"
              example:
                accepted: 1
                duplicates: 0
                rejected: []
                dead_lettered: 0
        "401":
          $ref: "#/components/responses/Unauthorized"
        "413":
          $ref: "#/components/responses/TooLarge"
        "422":
          $ref: "#/components/responses/Invalid"
        "429":
          $ref: "#/components/responses/RateLimited"
        "503":
          $ref: "#/components/responses/Unavailable"
  /api/ingest/otlp/v1/traces:
    post:
      tags:
        - OpenTelemetry
      operationId: ingestOtlpTraces
      summary: Send OpenTelemetry traces
      description: |
        An OTLP/HTTP trace receiver. Point an OpenTelemetry or OpenLIT exporter at this exact URL (set it as the **traces endpoint**, so the exporter does not append anything) and send the header `X-Ingest-Token`.

        Send protobuf (`application/x-protobuf`) or JSON (`application/json`); the response uses the same encoding. Gzip is accepted with `Content-Encoding: gzip`. Limits: **1 MB** per request (compressed and decompressed) and **600 spans per minute per token**. The token must be bound to a telemetry source of kind `otel`.

        **What is mapped.** An LLM span (`gen_ai.operation.name` of `chat`, `text_completion`, `completion`, `generate_content` or `embeddings`) becomes an `llm.call` event. A tool span (`gen_ai.operation.name` of `execute_tool`, or a span with `gen_ai.tool.name`) becomes a `tool.invocation` event. Every other span is dropped silently; that is not an error.

        **What is read.** Only a fixed allow-list of attributes: provider (`gen_ai.provider.name` or `gen_ai.system`), model (`gen_ai.response.model` or `gen_ai.request.model`), token counts (`gen_ai.usage.input_tokens` or `gen_ai.usage.prompt_tokens`, and `gen_ai.usage.output_tokens` or `gen_ai.usage.completion_tokens`), error status, agent (`gen_ai.agent.name`, else `service.name`), tool (`gen_ai.tool.name`) and `bairav.trace_id`. **Prompts, completions, messages, system instructions and tool arguments are never read or stored**, even if your exporter sends them. Cost estimates from the sender are ignored; Bairav prices usage from its own catalogue.

        A bad span is counted in `partialSuccess.rejectedSpans`; it never fails the whole request. The same span sent twice is one event.
      security:
        - IngestToken: []
        - IngestBearer: []
      requestBody:
        required: true
        content:
          application/x-protobuf:
            schema:
              type: string
              format: binary
              description: An OTLP `ExportTraceServiceRequest`.
          application/json:
            schema:
              type: object
              description: An OTLP/JSON `ExportTraceServiceRequest`.
              additionalProperties: true
      responses:
        "200":
          description: An OTLP `ExportTraceServiceResponse`, in the same encoding as the request.
          content:
            application/x-protobuf:
              schema:
                type: string
                format: binary
            application/json:
              schema:
                type: object
                additionalProperties: true
        "400":
          description: The body could not be parsed as OTLP.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "413":
          $ref: "#/components/responses/TooLarge"
        "415":
          description: The content type is not protobuf or JSON.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "429":
          $ref: "#/components/responses/RateLimited"
        "503":
          $ref: "#/components/responses/Unavailable"
  /api/ingest/sbom:
    post:
      tags:
        - Inventory
      operationId: ingestSbom
      summary: Upload a software bill of materials
      description: |
        Push an inventory for **one subject**. The body is the raw document. Supported formats: CycloneDX 1.4 to 1.6 JSON, SPDX 2.3 JSON, `requirements.txt` and `package-lock.json` (v2 and v3). Gzip is accepted with `Content-Encoding: gzip`.

        Limits: **20 MB** (compressed and decompressed) and **30 documents per minute per token**.

        The call is idempotent by subject and SHA-256: sending the same snapshot again answers `200` with `deduplicated: true` and changes nothing. The token must be bound to a source of kind `imported`, `sensor` or `infrastructure`, otherwise you get `403 token_not_allowed`.

        Errors carry a stable machine-readable `detail.code` such as `too_large`, `rate_limited`, `subject_not_found` or `invalid_subject`.
      security:
        - IngestToken: []
        - IngestBearer: []
      parameters:
        - name: subject_type
          in: query
          required: true
          schema:
            type: string
            enum:
              - agent
              - container_image
              - dependency_set
              - platform
        - name: subject_id
          in: query
          description: The agent or asset ID, when the subject already exists.
          schema:
            type: string
            maxLength: 64
        - name: subject_key
          in: query
          description: A stable key you choose, for example an image name. Used when you have no `subject_id`.
          schema:
            type: string
            maxLength: 300
        - name: subject_name
          in: query
          schema:
            type: string
            maxLength: 300
        - name: source
          in: query
          description: Where the document came from. Defaults to `self_inventory` for `platform` subjects and `ci` otherwise.
          schema:
            type: string
            enum:
              - ci
              - sensor
              - self_inventory
        - name: filename
          in: query
          schema:
            type: string
            maxLength: 200
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              description: A CycloneDX, SPDX or `package-lock.json` document.
              additionalProperties: true
          text/plain:
            schema:
              type: string
              description: A `requirements.txt` file.
      responses:
        "200":
          description: "Stored, or already stored (`deduplicated: true`)."
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/SbomResult"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          description: The `subject_id` does not exist in your tenant (`detail.code` is `subject_not_found`).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/CodedError"
        "413":
          $ref: "#/components/responses/TooLarge"
        "422":
          description: The document could not be parsed, or the subject is invalid. `detail.code` says which.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/CodedError"
        "429":
          $ref: "#/components/responses/RateLimited"
        "503":
          $ref: "#/components/responses/Unavailable"
  /api/ingest/health:
    get:
      tags:
        - Health
      operationId: ingestHealth
      summary: Check the ingest service
      description: Unauthenticated liveness check.
      responses:
        "200":
          description: The service is up.
          content:
            application/json:
              schema:
                type: object
                required:
                  - status
                  - service
                properties:
                  status:
                    type: string
                    const: ok
                  service:
                    type: string
                    const: ingest-api
components:
  securitySchemes:
    SensorToken:
      type: apiKey
      in: header
      name: X-Sensor-Token
      description: A sensor enrolment token, `bst_<8 hex>.<secret>`. Create one in Settings, Sensor deployment.
    IngestToken:
      type: apiKey
      in: header
      name: X-Ingest-Token
      description: An ingest token, `obt_<8 hex>.<secret>`. Create one in Settings, Security data and telemetry.
    IngestBearer:
      type: http
      scheme: bearer
      description: "The same ingest token, sent as `Authorization: Bearer obt_...`."
  responses:
    Unauthorized:
      description: The token is missing, malformed, unknown or revoked. Repeated failures for one token prefix are rate limited (30 a minute).
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"
    Forbidden:
      description: The token is valid but is bound to a telemetry source that may not use this endpoint.
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/CodedError"
    TooLarge:
      description: The body is larger than the endpoint limit. The size is checked before authentication and again while streaming.
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"
    Invalid:
      description: The body is not valid JSON, is empty, has too many events or uses an unsupported `schema_version`.
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"
    RateLimited:
      description: The per-token rate limit was exceeded. Wait `Retry-After` seconds.
      headers:
        Retry-After:
          description: Seconds to wait before retrying.
          schema:
            type: integer
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"
    Unavailable:
      description: The rate limiter or storage is temporarily unavailable. Retry after `Retry-After` seconds. The call is safe to repeat.
      headers:
        Retry-After:
          description: Seconds to wait before retrying.
          schema:
            type: integer
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"
  schemas:
    Error:
      type: object
      required:
        - detail
      properties:
        detail:
          description: A message, or a list of validation problems.
          oneOf:
            - type: string
            - type: array
              items:
                type: object
                additionalProperties: true
    CodedError:
      type: object
      required:
        - detail
      properties:
        detail:
          type: object
          required:
            - code
            - message
          properties:
            code:
              type: string
              examples:
                - too_large
            message:
              type: string
    SensorBatch:
      type: object
      required:
        - device_key
        - events
      properties:
        device_key:
          type: string
          description: A stable identifier for the host. It scopes the governance channel.
        events:
          type: array
          maxItems: 500
          minItems: 1
          items:
            $ref: "#/components/schemas/SensorEvent"
    SensorEvent:
      type: object
      required:
        - event_id
        - type
        - collected_at
        - payload
      properties:
        event_id:
          type: string
          description: Unique per event. Sending the same ID again is reported as a duplicate.
        type:
          type: string
          enum:
            - heartbeat
            - intake.inventory
            - agent.graph
            - agent.prompts
            - agent.activity
            - agent.reactivation_request
            - agent.guardrail_block
            - inventory.software
            - agent.config
        collected_at:
          type: string
          format: date-time
        payload:
          type: object
          description: Event-specific data. Must include `schema_version`.
          required:
            - schema_version
          properties:
            schema_version:
              type: string
              enum:
                - "1.0"
                - "1.1"
          additionalProperties: true
    RejectedEvent:
      type: object
      properties:
        index:
          type: integer
          description: Position of the event in your batch.
        event_id:
          type: string
        event_uid:
          type: string
        reason:
          type: string
    SensorIngestResult:
      type: object
      required:
        - accepted
        - duplicates
        - rejected
        - late
      properties:
        accepted:
          type: integer
        duplicates:
          type: integer
        rejected:
          type: array
          items:
            $ref: "#/components/schemas/RejectedEvent"
        late:
          type: integer
          description: How many accepted events arrived later than expected and were flagged `late`.
        warnings:
          type: array
          items:
            type: object
            additionalProperties: true
    GovernanceDirectives:
      type: object
      required:
        - agents
      properties:
        agents:
          type: array
          items:
            type: object
            required:
              - name
              - entity_type
              - state
              - guardrails
            properties:
              name:
                type: string
              entity_type:
                type: string
                enum:
                  - agent
                  - mcp_server
                  - llm
                  - ide
              kind:
                type:
                  - string
                  - "null"
              state:
                type: string
                description: "`active` unless a reviewer has suspended, blocked or rejected the agent."
              reason:
                type:
                  - string
                  - "null"
              since:
                type:
                  - string
                  - "null"
                format: date-time
              guardrails:
                type: array
                description: Guardrail policies attached to this agent. Enforce them per prompt and per tool call.
                items:
                  type: object
                  properties:
                    policy_id:
                      type: string
                    name:
                      type: string
                    action:
                      type: string
                    guardrail:
                      type: object
                      additionalProperties: true
              enforce:
                type: object
                description: Present only while the entity is on hold. Host-level hints for what to stop or refuse.
                additionalProperties: true
    SecurityEventBatch:
      type: object
      required:
        - schema_version
        - events
      properties:
        schema_version:
          type: string
          const: "1.0"
        events:
          type: array
          minItems: 1
          maxItems: 500
          items:
            $ref: "#/components/schemas/SecurityEvent"
    SecurityEvent:
      type: object
      required:
        - event_uid
        - occurred_at
        - event_type
      properties:
        event_uid:
          type: string
          maxLength: 200
          description: Your unique ID for the event. Idempotent per source.
        occurred_at:
          type: string
          format: date-time
        event_type:
          type: string
          description: Unknown types are rejected per event.
          enum:
            - gateway.request
            - policy.evaluation
            - policy.denied
            - tool.invocation
            - tool.unauthorized
            - mcp.connection
            - llm.call
            - egress.connection
            - agent.activity
            - agent.unregistered_activity
            - identity.owner_mismatch
            - auth.cross_tenant_attempt
            - guardrail.hit
            - session.started
            - session.ended
            - system.heartbeat
            - data.exposure
            - prompt.signal
        severity:
          type: string
          enum:
            - info
            - low
            - medium
            - high
            - critical
        observed_identity:
          type: string
          maxLength: 300
          description: Who or what produced the activity, for example `host:BTG-LAP-04/proc:python`.
        agent_version:
          type: string
          maxLength: 100
        trace_id:
          type: string
          maxLength: 128
        span_id:
          type: string
          maxLength: 64
        correlation_id:
          type:
            - string
            - "null"
          maxLength: 128
        decision:
          type:
            - string
            - "null"
        summary:
          type: string
        fields:
          type: object
          description: "Optional normalised fields, each masked, at most 8 KB per event: `host`, `port`, `endpoint`, `provider`, `model`, `tool`, `mcp_server`, `sdk`, `framework`, `user_agent`, `status`, `latency_ms`, `tokens_in`, `tokens_out`, `policy_id`, `rule`, `device`, `process`, `user`, `protocol`, `bytes_out`, `session_id`, `attributes`."
          additionalProperties: true
    SecurityIngestResult:
      type: object
      required:
        - accepted
        - duplicates
        - rejected
        - dead_lettered
      properties:
        accepted:
          type: integer
        duplicates:
          type: integer
        rejected:
          type: array
          items:
            $ref: "#/components/schemas/RejectedEvent"
        dead_lettered:
          type: integer
          description: Events that were valid but failed processing. They are retried automatically.
    SbomResult:
      type: object
      properties:
        deduplicated:
          type: boolean
          description: "`true` when the same snapshot for this subject was already stored."
      additionalProperties: true
