openapi: 3.1.0
info:
  title: Readable Universal Agent Traffic Ingestion API
  version: 0.1.0
  x-release-status: developer-preview
  x-plan-id: external-agentic-universal-ingestion
  x-plan-version: '1.1'
  description: >-
    Platform-neutral, server-to-server ingestion of privacy-bounded completed-request
    telemetry. Clients do not classify agents. Existing customer ingestion endpoints
    are separate and unchanged.
servers:
  - url: https://www.tryreadable.ai
paths:
  /api/external-agentic/traffic/batches:
    post:
      operationId: ingestExternalAgenticTrafficBatch
      summary: Submit a completed-request telemetry batch
      security:
        - bearerAuth: []
      parameters:
        - name: Idempotency-Key
          in: header
          required: true
          description: Must exactly equal the body batch_id and remain stable on retry.
          schema:
            type: string
            pattern: '^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$'
        - name: X-Readable-Ingestion-Version
          in: header
          required: false
          schema:
            type: string
            const: '1'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/TrafficBatch'
      responses:
        '200':
          description: Exact persisted, deduplicated, and rejected event accounting.
          headers:
            Cache-Control:
              schema:
                type: string
                const: private, no-store, max-age=0
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/BatchReceipt'
        '400':
          $ref: '#/components/responses/InvalidRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/IngestionDisabled'
        '409':
          $ref: '#/components/responses/Conflict'
        '413':
          $ref: '#/components/responses/PayloadTooLarge'
        '415':
          $ref: '#/components/responses/UnsupportedMediaType'
        '429':
          $ref: '#/components/responses/RateLimited'
        '503':
          $ref: '#/components/responses/TemporarilyUnavailable'
components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      description: Active site-scoped traffic:write-only credential bound to one registered universal integration.
  schemas:
    TrafficBatch:
      type: object
      additionalProperties: false
      required: [schema, version, site_id, batch_id, created_at, source, event_count, events]
      properties:
        schema:
          type: string
          const: readable.external-agentic.traffic
        version:
          type: integer
          const: 1
        site_id:
          type: string
          maxLength: 128
        batch_id:
          type: string
          pattern: '^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$'
        created_at:
          type: string
          format: date-time
        source:
          $ref: '#/components/schemas/Source'
        event_count:
          type: integer
          minimum: 1
          maximum: 500
        events:
          type: array
          minItems: 1
          maxItems: 500
          items:
            $ref: '#/components/schemas/TrafficEvent'
    Source:
      type: object
      additionalProperties: false
      required: [type, provider, integration_id, collector, collector_version]
      properties:
        type:
          type: string
          enum: [cdn_log, edge_event, server_log, application_event]
        provider:
          type: string
          pattern: '^[a-z0-9][a-z0-9._-]{0,63}$'
        integration_id:
          type: string
          description: Stable non-secret namespace registered by Readable and exactly bound to this credential, source type, and provider.
          pattern: '^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$'
        collector:
          type: string
          minLength: 1
          maxLength: 128
        collector_version:
          type: string
          minLength: 1
          maxLength: 64
    TrafficEvent:
      type: object
      additionalProperties: false
      required: [event_id, occurred_at, request, response]
      properties:
        event_id:
          type: string
          pattern: '^[A-Za-z0-9][A-Za-z0-9._:=+/-]{0,255}$'
        occurred_at:
          type: string
          format: date-time
        request:
          $ref: '#/components/schemas/RequestFacts'
        response:
          $ref: '#/components/schemas/ResponseFacts'
        delivery:
          oneOf:
            - $ref: '#/components/schemas/DeliveryFacts'
            - type: 'null'
        extensions:
          type: object
          description: Reserved. v0.1 accepts only an empty object.
          maxProperties: 0
    RequestFacts:
      type: object
      additionalProperties: false
      required: [request_id, method, scheme, hostname, path, user_agent]
      properties:
        request_id:
          type: string
          pattern: '^[A-Za-z0-9][A-Za-z0-9._:=+/-]{0,255}$'
        method:
          type: string
          pattern: '^[A-Z-]{3,16}$'
        scheme:
          type: string
          enum: [https]
        hostname:
          type: string
          maxLength: 255
        path:
          type: string
          maxLength: 16384
          pattern: '^/[^?#]*$'
        user_agent:
          type: string
          minLength: 1
          maxLength: 2048
        referrer_url:
          type: [string, 'null']
          maxLength: 2048
          description: Optional HTTP(S) URL without credentials, query, or fragment.
    ResponseFacts:
      type: object
      additionalProperties: false
      required: [status]
      properties:
        status:
          type: integer
          minimum: 100
          maximum: 599
        content_type:
          type: [string, 'null']
          maxLength: 512
        duration_ms:
          type: [integer, 'null']
          minimum: 0
        bytes_sent:
          type: [integer, 'null']
          minimum: 0
    DeliveryFacts:
      type: object
      additionalProperties: false
      required: [decision_id, requested_action, applied_action, served_by_readable, content_status, page_version, fallback_reason]
      properties:
        decision_id:
          type: [string, 'null']
          maxLength: 256
        requested_action:
          type: [string, 'null']
          enum: [serve, pass_through, null]
        applied_action:
          type: string
          enum: [serve, pass_through]
        served_by_readable:
          type: boolean
        content_status:
          type: [integer, 'null']
          minimum: 100
          maximum: 599
        page_version:
          type: [string, 'null']
          maxLength: 256
        fallback_reason:
          type: [string, 'null']
          maxLength: 128
    BatchReceipt:
      type: object
      additionalProperties: false
      required: [success, accepted, site_id, batch_id, event_count, inserted, deduplicated, rejected, errors]
      properties:
        success:
          type: boolean
          const: true
        accepted:
          type: boolean
          const: true
        site_id:
          type: string
        batch_id:
          type: string
        event_count:
          type: integer
        inserted:
          type: integer
        deduplicated:
          type: integer
        rejected:
          type: integer
        errors:
          type: array
          maxItems: 100
          items:
            type: object
            additionalProperties: false
            required: [index, code]
            properties:
              index:
                type: integer
              event_id:
                type: string
              code:
                type: string
        errors_truncated:
          type: boolean
          const: true
    ErrorResponse:
      type: object
      additionalProperties: false
      required: [success, error]
      properties:
        success:
          type: boolean
          const: false
        error:
          type: object
          additionalProperties: false
          required: [code, message]
          properties:
            code:
              type: string
            message:
              type: string
  responses:
    InvalidRequest:
      description: Malformed JSON, missing idempotency, or invalid envelope.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
    Unauthorized:
      description: Missing, invalid, inactive, non-traffic, or integration-unbound credential.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
    IngestionDisabled:
      description: Traffic ingestion is disabled for the credential-bound site, or the registered integration/source identity is not authorized.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
    Conflict:
      description: The batch is in flight or its ID was reused for different content.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
    PayloadTooLarge:
      description: The JSON body exceeds one MiB.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
    UnsupportedMediaType:
      description: The media type or content encoding is unsupported. v0.1 launches without gzip.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
    RateLimited:
      description: The credential or site exceeded the protective batch submission limit.
      headers:
        Retry-After:
          schema:
            type: integer
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
    TemporarilyUnavailable:
      description: A temporary dependency or persistence failure.
      headers:
        Retry-After:
          schema:
            type: integer
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
