openapi: 3.1.0
info:
  title: ClickHouse Product Analytics Ingest API
  version: 0.1.0
  description: |
    Public HTTP contract for the ClickHouse Product Analytics ingest service.
    The service has one event ingestion endpoint. Single-event ingestion uses
    the same endpoint with a one-item batch.
servers:
  - url: http://127.0.0.1:8080
    description: Local development ingest service
tags:
  - name: Ingest
    description: Event ingestion
  - name: Health
    description: Service health
paths:
  /batch/:
    post:
      tags:
        - Ingest
      operationId: ingestBatch
      summary: Ingest a batch of events
      description: |
        Accepts an application/json object with a non-empty `batch` array.
        Browser requests from allowed origins can omit `api_key`. Backend or
        no-origin requests require a valid `api_key` from `PUBLIC_API_KEYS`.

        Unknown top-level and event-level fields are accepted but ignored
        unless they are inside `properties`. Events missing `event` or a
        distinct ID are dropped and counted in the response instead of failing
        the whole request. A distinct ID may be sent as `distinct_id`,
        `properties.distinct_id`, or `properties.$distinct_id`.

        Request compression is supported only with `Content-Encoding: gzip`.
        Other encodings and the `compression` query parameter are rejected.
      parameters:
        - name: Origin
          in: header
          required: false
          schema:
            type: string
            format: uri
          description: Browser origin. When present, it must exactly match `ALLOWED_ORIGINS`.
        - name: Content-Encoding
          in: header
          required: false
          schema:
            type: string
            enum:
              - gzip
          description: Optional gzip request compression. Omit the header for uncompressed JSON.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/BatchRequest'
            examples:
              backend:
                summary: Backend one-event batch
                value:
                  api_key: local_dev_key
                  batch:
                    - event: backend_job_completed
                      distinct_id: user_123
                      properties:
                        job_id: job_456
                        duration_ms: 481
              browser:
                summary: Browser-origin request without api_key
                value:
                  batch:
                    - event: $pageview
                      distinct_id: anon_123
                      properties:
                        $current_url: https://example.com/
                        $session_id: session_123
              dropped:
                summary: Mixed valid and dropped events
                value:
                  api_key: local_dev_key
                  batch:
                    - event: signup_started
                      distinct_id: user_123
                    - event: missing_distinct_id
      responses:
        '200':
          description: Request accepted. `dropped` reports events skipped because required event-level fields were missing.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/IngestSuccess'
              examples:
                ok:
                  value:
                    status: ok
                    ingested: 1
                    dropped: 0
                withDroppedEvents:
                  value:
                    status: ok
                    ingested: 1
                    dropped: 1
        '400':
          description: Malformed JSON, invalid payload shape, invalid timestamp, or mixed api_key values.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              examples:
                payload:
                  value:
                    status: error
                    error: Payload must be a JSON object with a non-empty batch array
                mixedKeys:
                  value:
                    status: error
                    error: Mixed api_key values in one request are not supported
        '401':
          description: Missing key on a no-origin backend request, unknown backend key, or invalid provided browser key.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              examples:
                invalidKey:
                  value:
                    status: error
                    error: Invalid api_key
        '403':
          description: The browser request origin is not allowed.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              examples:
                origin:
                  value:
                    status: error
                    error: Origin is not allowed
        '413':
          description: The request body exceeds `MAX_BATCH_BYTES` or the batch exceeds `MAX_EVENTS_PER_BATCH`.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              examples:
                events:
                  value:
                    status: error
                    error: Batch has 10001 events, maximum is 10000
        '415':
          description: Unsupported content type, unsupported content encoding, or unsupported compression query parameter.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              examples:
                contentType:
                  value:
                    status: error
                    error: Unsupported content type. Use application/json.
                encoding:
                  value:
                    status: error
                    error: 'Unsupported content-encoding: br'
                compressionQuery:
                  value:
                    status: error
                    error: 'The compression query parameter is not supported. Use Content-Encoding: gzip.'
        '500':
          description: Unexpected server or ClickHouse write error.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
  /health:
    get:
      tags:
        - Health
      operationId: getHealth
      summary: Check service health
      responses:
        '200':
          description: Service is alive.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/HealthResponse'
              examples:
                ok:
                  value:
                    ok: true
components:
  schemas:
    BatchRequest:
      type: object
      required:
        - batch
      additionalProperties: true
      properties:
        api_key:
          type: string
          description: Optional ingest credential. Required for backend or no-origin requests when backend ingest is enabled.
        batch:
          type: array
          minItems: 1
          items:
            $ref: '#/components/schemas/IngestEvent'
        historical_migration:
          type: boolean
          description: Optional accepted flag reserved for migration callers. It is currently accepted and ignored.
      description: Unknown top-level fields are accepted and ignored.
    IngestEvent:
      type: object
      additionalProperties: true
      properties:
        api_key:
          type: string
          description: Optional event-level ingest credential. If multiple keys are provided in one request, all values must match.
        event:
          type: string
          description: Required for this event to be ingested. Missing or empty values drop the event.
        distinct_id:
          type: string
          description: Required for this event to be ingested unless provided as `properties.distinct_id` or `properties.$distinct_id`.
        timestamp:
          type: string
          format: date-time
          description: Optional event timestamp. Defaults to ingest time. Invalid timestamps return 400.
        properties:
          type: object
          additionalProperties: true
          description: Arbitrary event properties stored in ClickHouse. Recognized keys such as `$session_id`, `$window_id`, `$current_url`, `$host`, `$anon_distinct_id`, `$set`, and `$set_once` drive normalized columns and identity side effects.
      description: Unknown event-level fields are accepted and ignored unless they are inside `properties`.
    IngestSuccess:
      type: object
      required:
        - status
        - ingested
        - dropped
      properties:
        status:
          type: string
          const: ok
        ingested:
          type: integer
          minimum: 0
        dropped:
          type: integer
          minimum: 0
    HealthResponse:
      type: object
      required:
        - ok
      properties:
        ok:
          type: boolean
          const: true
    ErrorResponse:
      type: object
      required:
        - status
        - error
      properties:
        status:
          type: string
          const: error
        error:
          type: string
