openapi: 3.0.0
paths:
  /api/v1/workspaces/{workspaceId}/signals:
    get:
      description: >-
        Returns signals defined for the workspace, optionally filtered by active
        status.
      operationId: WorkspaceSignalsController_findAll
      parameters:
        - name: workspaceId
          required: true
          in: path
          description: Workspace UUID
          schema:
            format: uuid
            type: string
        - name: isActive
          required: false
          in: query
          description: When provided, only returns signals matching this active state.
          schema:
            type: boolean
      responses:
        '200':
          description: The list of signals for the workspace.
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/SignalResponse'
        '401':
          description: Missing or invalid API key (or session token in the web app).
        '403':
          description: >-
            No access to this workspace: an API key needs the signals:read scope
            and must belong to it.
        '404':
          description: No workspace exists with this ID.
      security:
        - ApiKeyAuth: []
      summary: List workspace signals
      tags:
        - Read data
  /api/v1/signals/{id}:
    get:
      description: Returns a single signal by its UUID.
      operationId: SignalsController_findOne
      parameters:
        - name: id
          required: true
          in: path
          description: Signal UUID
          schema:
            format: uuid
            type: string
      responses:
        '200':
          description: The requested signal.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SignalResponse'
        '401':
          description: Missing or invalid API key (or session token in the web app).
        '403':
          description: >-
            No access to this signal's workspace: an API key needs the
            signals:read scope and must belong to it.
        '404':
          description: No signal exists with this ID.
      security:
        - ApiKeyAuth: []
      summary: Get a signal by ID
      tags:
        - Read data
  /api/v1/workspaces/{workspaceId}/telemetry/query:
    post:
      description: >-
        Powers both the live-tail inspector (a rolling recent window, polled by
        the client) and historical browsing (an arbitrary date/time range) over
        the same partitioned Parquet files, optionally filtered by signal, tag
        values, or a free-text tag search.
      operationId: WorkspaceTelemetryController_query
      parameters:
        - name: workspaceId
          required: true
          in: path
          description: Workspace UUID
          schema:
            format: uuid
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/TelemetryQueryRequestDto'
      responses:
        '200':
          description: The matching events, most recent first.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/TelemetryQueryResponse'
        '401':
          description: Missing or invalid API key (or session token in the web app).
        '403':
          description: >-
            No access to this workspace: an API key needs the signals:read scope
            and must belong to it.
      security:
        - ApiKeyAuth: []
      summary: Query raw telemetry events
      tags:
        - Read data
  /api/v1/ingest:
    post:
      description: >-
        Accepts a single event payload or a batch (array, up to 500 items). Each
        payload is validated against the resolved workspace's registered signal
        and tag schema before being queued for asynchronous Parquet storage.
        Batches are all-or-nothing: if any payload fails validation, the entire
        request is rejected and nothing is queued.
      operationId: IngestionController_ingest
      parameters: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/IngestRequestDto'
      responses:
        '201':
          description: The event(s) were validated and queued for storage.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/IngestResponse'
        '400':
          description: >-
            One or more payloads targeted an unknown/inactive signal or failed
            tag schema validation.
        '401':
          description: Missing, invalid, or expired API key.
        '403':
          description: >-
            The API key is missing the signals:emit scope or is not scoped to a
            workspace.
      security:
        - ApiKeyAuth: []
      summary: Ingest one or more telemetry events
      tags:
        - Send data
  /api/v1/workspaces/{workspaceId}/reports/query:
    post:
      description: >-
        Runs a time-bucketed aggregation (with an optional dimension breakdown,
        capped at the top 10 values plus an 'Other' bucket) over the workspace's
        partitioned Parquet files for the given signal and date range.
      operationId: ReportsController_query
      parameters:
        - name: workspaceId
          required: true
          in: path
          description: Workspace UUID
          schema:
            format: uuid
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ReportQueryRequestDto'
      responses:
        '200':
          description: The aggregated report result.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ReportQueryResponse'
        '400':
          description: >-
            startDate is not before endDate, or groupByDimension is not a
            registered tag on this signal.
        '401':
          description: Missing or invalid API key (or session token in the web app).
        '403':
          description: >-
            No access to this workspace: an API key needs the signals:read scope
            and must belong to it.
      security:
        - ApiKeyAuth: []
      summary: Run a historical report query
      tags:
        - Read data
  /api/v1/otlp/v1/metrics:
    post:
      description: >-
        Accepts a standard OTLP ExportMetricsServiceRequest (JSON body only —
        Protobuf is not yet supported). Gauge and Sum data points whose metric
        name matches an enabled mapping rule for the resolved workspace are
        transformed into Datius signal events and queued the same way as manual
        ingestion; unmatched metrics and unsupported point types
        (Histogram/Summary/ExponentialHistogram) are silently ignored, per
        OTLP's own tolerance for partial acceptance.
      operationId: OtlpMetricsController_exportMetrics
      parameters: []
      responses:
        '200':
          description: >-
            The payload was structurally valid and any matching data points were
            queued for storage.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OtlpExportMetricsResponse'
        '400':
          description: The request body isn't a well-formed OTLP metrics export.
        '401':
          description: Missing, invalid, or expired API key.
        '403':
          description: >-
            The API key is missing the signals:emit scope or is not scoped to a
            workspace.
      security:
        - ApiKeyAuth: []
      summary: OTLP/HTTP+JSON metrics ingestion
      tags:
        - Send data
  /api/v1/otlp/v1/traces:
    post:
      description: >-
        Accepts a standard OTLP ExportTraceServiceRequest (JSON body only —
        Protobuf is not yet supported). A span's duration (endTimeUnixNano −
        startTimeUnixNano, in milliseconds) becomes the signal value for any
        span whose name matches an enabled trace mapping rule for the resolved
        workspace, timestamped at the span's end time; unmatched span names are
        silently ignored, per OTLP's own tolerance for partial acceptance.
      operationId: OtlpMetricsController_exportTraces
      parameters: []
      responses:
        '200':
          description: >-
            The payload was structurally valid and any matching spans were
            queued for storage.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OtlpExportTracesResponse'
        '400':
          description: The request body isn't a well-formed OTLP traces export.
        '401':
          description: Missing, invalid, or expired API key.
        '403':
          description: >-
            The API key is missing the signals:emit scope or is not scoped to a
            workspace.
      security:
        - ApiKeyAuth: []
      summary: OTLP/HTTP+JSON traces ingestion
      tags:
        - Send data
info:
  title: Datius API
  description: >-
    Send events to Datius and read your data back. Every request is
    authenticated with a workspace API key, created in the app under API Keys.
  version: 1.0.0
  contact: {}
tags:
  - name: Send data
    description: >-
      Send events to Datius from your code, the Node.js SDK or an OpenTelemetry
      collector. Needs an API key with the signals:emit scope.
  - name: Read data
    description: >-
      Read your signals, aggregated report numbers and raw events. Needs an API
      key with the signals:read scope, belonging to the workspace in the URL.
servers: []
components:
  securitySchemes:
    ApiKeyAuth:
      type: apiKey
      in: header
      name: X-API-Key
      description: >-
        A signals:emit-scoped API key. Authorization: Bearer <key> is also
        accepted.
  schemas:
    SignalResponse:
      type: object
      properties:
        id:
          type: string
          format: uuid
          pattern: >-
            ^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$
        workspaceId:
          type: string
          format: uuid
          pattern: >-
            ^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$
        key:
          type: string
        displayName:
          type: string
        description:
          type: array
          items:
            type: string
        dataType:
          type: string
          enum:
            - counter
            - numeric
            - currency
            - percentage
            - duration
        durationUnit:
          type: string
          enum:
            - ms
            - s
            - min
        tagSchema:
          type: array
          items:
            type: object
            properties:
              key:
                type: string
                minLength: 1
                maxLength: 100
              type:
                type: string
                enum:
                  - string
                  - number
                  - boolean
              isRequired:
                type: boolean
            required:
              - key
              - type
              - isRequired
            additionalProperties: false
        isActive:
          type: boolean
        hasData:
          type: boolean
        createdAt:
          type: string
          format: date-time
          pattern: >-
            ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z))$
        updatedAt:
          type: string
          format: date-time
          pattern: >-
            ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z))$
      required:
        - id
        - workspaceId
        - key
        - displayName
        - description
        - dataType
        - durationUnit
        - tagSchema
        - isActive
        - hasData
        - createdAt
        - updatedAt
    TelemetryQueryRequestDto:
      type: object
      properties:
        mode:
          type: string
          enum:
            - live
            - historical
        signalKey:
          type: string
          minLength: 1
        startTime:
          type: string
          format: date-time
          pattern: >-
            ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z))$
        endTime:
          type: string
          format: date-time
          pattern: >-
            ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z))$
        tagFilters:
          type: array
          items:
            type: object
            properties:
              key:
                type: string
                minLength: 1
              value:
                type: string
                minLength: 1
            required:
              - key
              - value
        search:
          type: array
          items:
            type: string
            minLength: 1
        limit:
          default: 50
          type: integer
          minimum: 1
          maximum: 500
        offset:
          default: 0
          type: integer
          minimum: 0
          maximum: 9007199254740991
      required:
        - mode
        - startTime
        - endTime
      additionalProperties: false
    TelemetryEventResponse:
      type: object
      properties:
        eventId:
          type: string
          description: >-
            Derived at query time from the event's signal key and its own
            microsecond timestamp — not a stored identifier.
          example: evt_checkout.gross_revenue_1789055641150000_1
        signalKey:
          type: string
          example: checkout.gross_revenue
        value:
          type: number
          example: 149.99
        timestamp:
          type: string
          description: A UTC ISO-8601 timestamp.
          example: '2026-09-10T13:42:01.5Z'
        tags:
          type: object
          description: The event's tag dimensions.
          example:
            store_id: store-101
        parquetPath:
          type: string
          description: >-
            The object storage key this event was read from, from DuckDB's
            filename metadata column.
          example: >-
            s3://datius-local-bucket/orgs/.../workspaces/.../signals/checkout.gross_revenue/year=2026/month=09/day=10/part-1789055641150-....parquet
      required:
        - eventId
        - signalKey
        - value
        - timestamp
        - tags
        - parquetPath
    TelemetryQueryResponse:
      type: object
      properties:
        events:
          type: array
          items:
            $ref: '#/components/schemas/TelemetryEventResponse'
      required:
        - events
    IngestRequestDto:
      anyOf:
        - type: object
          properties:
            signalKey:
              type: string
              minLength: 1
            value:
              type: number
            timestamp:
              type: string
              format: date-time
              pattern: >-
                ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z))$
            tags:
              type: object
              additionalProperties:
                type:
                  - string
                  - number
                  - boolean
          required:
            - signalKey
            - value
          additionalProperties: false
        - minItems: 1
          maxItems: 500
          type: array
          items:
            type: object
            properties:
              signalKey:
                type: string
                minLength: 1
              value:
                type: number
              timestamp:
                type: string
                format: date-time
                pattern: >-
                  ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z))$
              tags:
                type: object
                additionalProperties:
                  type:
                    - string
                    - number
                    - boolean
            required:
              - signalKey
              - value
            additionalProperties: false
    IngestResponse:
      type: object
      properties:
        accepted:
          type: number
          description: Number of events accepted and queued for storage.
          example: 1
      required:
        - accepted
    ReportQueryRequestDto:
      type: object
      properties:
        signalKey:
          type: string
          minLength: 1
        startDate:
          type: string
          format: date-time
          pattern: >-
            ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z))$
        endDate:
          type: string
          format: date-time
          pattern: >-
            ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z))$
        granularity:
          type: string
          enum:
            - 1h
            - 1d
            - 1w
            - 1m
        aggregation:
          type: string
          enum:
            - count
            - sum
            - avg
            - min
            - max
            - p95
            - p99
        groupByDimension:
          type: string
          minLength: 1
        tagFilters:
          type: array
          items:
            type: object
            properties:
              key:
                type: string
                minLength: 1
              value:
                type: string
                minLength: 1
            required:
              - key
              - value
        timezoneOffsetMinutes:
          type: integer
          minimum: -840
          maximum: 840
      required:
        - signalKey
        - startDate
        - endDate
        - granularity
        - aggregation
      additionalProperties: false
    ReportSeriesPointResponse:
      type: object
      properties:
        bucket:
          type: string
          example: '2026-09-10T00:00:00.000Z'
        dimensionValue:
          type: object
          example: gold
          nullable: true
        metricValue:
          type: object
          example: 412500
          nullable: true
        eventCount:
          type: number
          example: 14210
      required:
        - bucket
        - dimensionValue
        - metricValue
        - eventCount
    ReportSummaryResponse:
      type: object
      properties:
        totalEvents:
          type: number
          example: 25780
        aggregatedValue:
          type: object
          example: 708800.5
          nullable: true
        peakBucket:
          type: object
          example: '2026-09-02T00:00:00.000Z'
          nullable: true
        peakValue:
          type: object
          example: 48210
          nullable: true
        averagePerBucket:
          type: object
          example: 23626.7
          nullable: true
      required:
        - totalEvents
        - aggregatedValue
        - peakBucket
        - peakValue
        - averagePerBucket
    ReportBreakdownRowResponse:
      type: object
      properties:
        dimensionValue:
          type: string
          example: gold
        totalEvents:
          type: number
          example: 14210
        aggregatedValue:
          type: object
          example: 412500
          nullable: true
        percentShare:
          type: number
          example: 58.2
        peakBucket:
          type: object
          example: '2026-09-02T00:00:00.000Z'
          nullable: true
      required:
        - dimensionValue
        - totalEvents
        - aggregatedValue
        - percentShare
        - peakBucket
    ReportQueryResponse:
      type: object
      properties:
        status:
          type: string
          enum:
            - OK
            - SIGNAL_PURGED
            - INVALID_FILTER
          description: >-
            SIGNAL_PURGED means the requested signalKey no longer resolves to a
            signal in this workspace; INVALID_FILTER means a tag filter names a
            tag this signal does not declare — in both cases every other field
            is omitted.
        signalKey:
          type: string
          description: Echoed back only when status is SIGNAL_PURGED or INVALID_FILTER.
          example: order_completed
        invalidTags:
          description: >-
            Only with status INVALID_FILTER: the filtered tag keys this signal
            does not declare.
          example:
            - store_id
          type: array
          items:
            type: string
        series:
          type: array
          items:
            $ref: '#/components/schemas/ReportSeriesPointResponse'
        summary:
          $ref: '#/components/schemas/ReportSummaryResponse'
        breakdown:
          type: array
          items:
            $ref: '#/components/schemas/ReportBreakdownRowResponse'
      required:
        - status
        - signalKey
        - invalidTags
        - series
        - summary
        - breakdown
    OtlpExportMetricsResponse:
      type: object
      properties: {}
    OtlpExportTracesResponse:
      type: object
      properties: {}
