{
  "openapi": "3.0.3",
  "info": {
    "title": "Epok Public API",
    "version": "1.0.0",
    "description": "Curated public API surface for Epok.\n\nThis spec documents the stable, supported endpoints customers use programmatically. Admin, OAuth, team-management, and internal routes are intentionally excluded.\n\n## Authentication\n\nAll non-`/health` requests require a tenant-scoped API key. Three transport options:\n\n- `Authorization: Bearer <key>` header (recommended for HTTP)\n- HTTP Basic Auth (username = key, password = empty) — used by Loki-native shippers\n- `X-API-Key: <key>` header — used by some legacy clients\n\nKeys have one or more scopes: `read` (default), `write`, `ingest`. Endpoints that mutate state require `write`.\n\nBrowser sessions authenticate via secure cookies, which are not part of the public API contract.\n\n## Rate limits\n\nQuery endpoints (`/search`, `/hits`, `/facets`, `/streams`, `/patterns`) are rate-limited per API key. When limits are exceeded, the API returns `429 Too Many Requests` with a `Retry-After` header (seconds). Ingest endpoints have separate per-tenant token-bucket limits configured by plan tier.\n\n## Errors\n\nEvery error response uses the `ErrorResponse` schema and includes an `X-Request-Id` response header for support correlation.\n\n## Pagination\n\nList endpoints return `items`, `total`, `limit`, `offset`, `has_more`. Page by incrementing `offset` by `limit` until `has_more` is `false`.\n\n## Time format\n\nAll timestamps are ISO-8601 UTC (`2026-05-14T12:00:00Z`). Query window parameters (`start`, `end`, `since`) additionally accept relative durations like `-1h`, `-15m`, `-7d`, and the literal `now`.\n\n## Servers\n\n- `https://app.getepok.dev` — API + WebSocket endpoints\n- `https://ingest.getepok.dev` — logs, metrics, traces, and RUM ingestion endpoints",
    "contact": {
      "name": "Epok Support",
      "email": "support@getepok.dev",
      "url": "https://epok.cloud/docs"
    },
    "license": {
      "name": "Proprietary",
      "url": "https://epok.cloud/terms"
    }
  },
  "servers": [
    {
      "url": "https://app.getepok.dev",
      "description": "API + WebSocket endpoints"
    },
    {
      "url": "https://ingest.getepok.dev",
      "description": "Telemetry ingestion — logs, metrics, traces, and RUM"
    }
  ],
  "security": [
    {
      "BearerAuth": []
    },
    {
      "BasicAuth": []
    },
    {
      "ApiKeyHeader": []
    }
  ],
  "tags": [
    {
      "name": "Health",
      "description": "Unauthenticated health check."
    },
    {
      "name": "Ingest",
      "description": "Telemetry ingestion on `ingest.getepok.dev` — logs, metrics, traces, and RUM. OTLP (HTTP/JSON or protobuf) carries logs, metrics, and traces; browser RUM rides the same OTLP metrics + traces endpoints, and session replay posts rrweb chunks to `/v1/rum/replay`. Logs are also wire-compatible with Elasticsearch Bulk, Loki Push, and syslog; metrics also accept Prometheus remote_write and InfluxDB line protocol."
    },
    {
      "name": "Search",
      "description": "Search logs, fetch facets, fetch volume histograms, list streams."
    },
    {
      "name": "Metrics",
      "description": "Query the metric store — MetricsQL/PromQL instant + range queries, label discovery, and cardinality. Read scope."
    },
    {
      "name": "Traces",
      "description": "Search distributed traces and fetch a single trace's spans (OTLP, correlated by trace ID). Read scope."
    },
    {
      "name": "RUM",
      "description": "Real-user monitoring — session-replay ingest and the session query surface. Browser web-vitals and errors arrive as OTLP metrics/traces (see Ingest); rrweb session replay has its own ingest + query paths."
    },
    {
      "name": "Alerts",
      "description": "List, inspect, and resolve alerts. All paths are tenant-scoped."
    },
    {
      "name": "Intelligence",
      "description": "Pattern clusters produced by the intelligence engine. Each pattern groups variant log messages under a stable fingerprint."
    },
    {
      "name": "Live",
      "description": "WebSocket streams for live-tail and real-time alert updates."
    }
  ],
  "paths": {
    "/health": {
      "get": {
        "tags": [
          "Health"
        ],
        "summary": "Service health check",
        "description": "Returns 200 when the API process is running. Does not check downstream dependencies (database, log backend). Suitable for HTTP load-balancer health checks.",
        "operationId": "getHealth",
        "security": [],
        "responses": {
          "200": {
            "description": "Service is up.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/HealthResponse"
                },
                "example": {
                  "status": "ok"
                }
              }
            }
          }
        }
      }
    },
    "/insert/elasticsearch/_bulk": {
      "post": {
        "tags": [
          "Ingest"
        ],
        "summary": "Elasticsearch-compatible bulk ingest",
        "description": "Accepts NDJSON with Elasticsearch-style bulk action lines. Wire-compatible with Logstash, Filebeat, Vector's `elasticsearch` sink, Fluent Bit's `es` output, and plain curl.\n\nSend on `ingest.getepok.dev`. Each pair of lines is `{\"create\":{}}` followed by the log entry JSON.",
        "operationId": "ingestElasticsearchBulk",
        "requestBody": {
          "required": true,
          "content": {
            "application/x-ndjson": {
              "schema": {
                "type": "string",
                "format": "ndjson"
              },
              "example": "{\"create\":{}}\n{\"_msg\":\"GET /api/users 200 42ms\",\"_time\":\"2026-05-14T10:00:00Z\",\"level\":\"info\",\"service\":\"api\",\"status_code\":200}\n{\"create\":{}}\n{\"_msg\":\"connection timeout\",\"_time\":\"2026-05-14T10:00:01Z\",\"level\":\"error\",\"service\":\"api\"}\n"
            }
          }
        },
        "responses": {
          "200": {
            "description": "All entries accepted.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/IngestAck"
                },
                "example": {
                  "took": 4,
                  "errors": false,
                  "items": []
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "413": {
            "$ref": "#/components/responses/PayloadTooLarge"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/loki/api/v1/push": {
      "post": {
        "tags": [
          "Ingest"
        ],
        "summary": "Loki push API",
        "description": "Loki-compatible push endpoint on `ingest.getepok.dev`. Wire-compatible with Promtail, Grafana Alloy, Fluent Bit `loki` output, Vector's `loki` sink, and the OpenTelemetry Loki exporter.\n\nUse HTTP Basic Auth (username = API key, password = empty) — this is what Loki shippers send by default.",
        "operationId": "ingestLokiPush",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/LokiPushBody"
              },
              "example": {
                "streams": [
                  {
                    "stream": {
                      "service": "api",
                      "host": "web-01",
                      "level": "info"
                    },
                    "values": [
                      [
                        "1715680800000000000",
                        "GET /api/users 200 42ms"
                      ],
                      [
                        "1715680801000000000",
                        "GET /api/orders 200 18ms"
                      ]
                    ]
                  }
                ]
              }
            }
          }
        },
        "responses": {
          "204": {
            "description": "Accepted."
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "413": {
            "$ref": "#/components/responses/PayloadTooLarge"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/v1/logs": {
      "post": {
        "tags": [
          "Ingest"
        ],
        "summary": "OpenTelemetry HTTP logs (OTLP)",
        "description": "OpenTelemetry Protocol HTTP logs endpoint on `ingest.getepok.dev`. Use with the `otlphttp` exporter in the OpenTelemetry Collector, the `OTEL_EXPORTER_OTLP_LOGS_ENDPOINT` env var on any OTEL SDK, or any OTLP-HTTP-compatible shipper.\n\nAccepts both `application/json` (OTLP/HTTP JSON) and `application/x-protobuf` (OTLP/HTTP binary).",
        "operationId": "ingestOtlpLogs",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object"
              },
              "example": {
                "resourceLogs": [
                  {
                    "resource": {
                      "attributes": [
                        {
                          "key": "service.name",
                          "value": {
                            "stringValue": "api"
                          }
                        }
                      ]
                    },
                    "scopeLogs": [
                      {
                        "logRecords": [
                          {
                            "timeUnixNano": "1715680800000000000",
                            "severityText": "INFO",
                            "body": {
                              "stringValue": "GET /api/users 200 42ms"
                            }
                          }
                        ]
                      }
                    ]
                  }
                ]
              }
            },
            "application/x-protobuf": {
              "schema": {
                "type": "string",
                "format": "binary"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Accepted."
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "413": {
            "$ref": "#/components/responses/PayloadTooLarge"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/api/v1/syslog": {
      "post": {
        "tags": [
          "Ingest"
        ],
        "summary": "Syslog over HTTP",
        "description": "Accepts RFC 5424 and RFC 3164 syslog frames as the request body, newline-delimited for batched submission. Use with rsyslog `omhttp`, syslog-ng `http()`, or any HTTP-syslog forwarder.",
        "operationId": "ingestSyslogHttp",
        "requestBody": {
          "required": true,
          "content": {
            "text/plain": {
              "schema": {
                "type": "string"
              },
              "example": "<134>1 2026-05-14T10:00:00Z web-01 nginx 1234 - - GET /api/users 200 42ms\n<131>1 2026-05-14T10:00:01Z web-01 api 1234 - - connection timeout\n"
            }
          }
        },
        "responses": {
          "200": {
            "description": "Accepted."
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "413": {
            "$ref": "#/components/responses/PayloadTooLarge"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/api/v1/tenants/{tenant_id}/search": {
      "post": {
        "tags": [
          "Search"
        ],
        "summary": "Search logs",
        "description": "Full-text and field search over the tenant's log store. Uses Epok's search syntax — see [the search reference](https://epok.cloud/docs/query) for operators and pipes.\n\nSupports cursor-style pagination: pass the last entry's `_time` as `before` to fetch older logs.",
        "operationId": "searchLogs",
        "parameters": [
          {
            "$ref": "#/components/parameters/TenantId"
          },
          {
            "name": "enrich",
            "in": "query",
            "description": "Enrich error entries with first-occurrence fingerprint metadata.",
            "required": false,
            "schema": {
              "type": "boolean",
              "default": true
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/SearchRequest"
              },
              "example": {
                "query": "level:error service:api",
                "start": "-1h",
                "end": "now",
                "limit": 100
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Matching log entries (newest first).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SearchResponse"
                },
                "example": {
                  "logs": [
                    {
                      "_time": "2026-05-14T10:00:01.234Z",
                      "_msg": "connection timeout after 30s on port 5432",
                      "_stream": "{service=\"api\",host=\"web-01\"}",
                      "level": "error",
                      "service": "api"
                    }
                  ],
                  "count": 1,
                  "elapsed_ms": 142,
                  "has_more": false,
                  "oldest_time": "2026-05-14T10:00:01.234Z",
                  "meta": {
                    "effective_window": {
                      "requested_start": "-1h",
                      "requested_end": "now",
                      "effective_start": "2026-05-14T09:00:00Z",
                      "effective_end": "2026-05-14T10:00:00Z",
                      "clamped_by": null
                    },
                    "completeness": {
                      "state": "complete",
                      "reason": null
                    },
                    "warnings": [],
                    "freshness": null,
                    "limits": {
                      "applied_limit": 1000,
                      "truncated": false,
                      "continuation": null
                    },
                    "provenance": {
                      "signal": "logs",
                      "schema_version": "1.0",
                      "query_ref": "qr_9f2c41a7b8d0e315"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "504": {
            "$ref": "#/components/responses/Timeout"
          }
        }
      }
    },
    "/api/v1/tenants/{tenant_id}/metrics/query": {
      "get": {
        "tags": [
          "Metrics"
        ],
        "summary": "Instant metric query (MetricsQL)",
        "description": "Evaluate a MetricsQL/PromQL-compatible expression at a single instant against the tenant's metric store.",
        "operationId": "metricsQuery",
        "parameters": [
          {
            "$ref": "#/components/parameters/TenantId"
          },
          {
            "name": "query",
            "in": "query",
            "required": true,
            "description": "MetricsQL/PromQL expression.",
            "schema": {
              "type": "string"
            },
            "example": "rate(http_requests_total[5m])"
          },
          {
            "name": "time",
            "in": "query",
            "required": false,
            "description": "Evaluation instant (ISO-8601, Unix epoch, or relative like `-5m`). Defaults to now.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Instant-vector result.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data"
                  ],
                  "properties": {
                    "data": {
                      "type": "object",
                      "description": "Prometheus-compatible result object."
                    },
                    "meta": {
                      "$ref": "#/components/schemas/ResultMeta"
                    }
                  }
                },
                "example": {
                  "data": {
                    "resultType": "vector",
                    "result": [
                      {
                        "metric": {
                          "__name__": "http_requests_total",
                          "service": "api"
                        },
                        "value": [
                          1747219200,
                          "12.4"
                        ]
                      }
                    ]
                  },
                  "meta": {
                    "effective_window": {
                      "requested_start": "-1h",
                      "requested_end": "now",
                      "effective_start": "2026-05-14T09:00:00Z",
                      "effective_end": "2026-05-14T10:00:00Z",
                      "clamped_by": null
                    },
                    "completeness": {
                      "state": "complete",
                      "reason": null
                    },
                    "warnings": [],
                    "freshness": null,
                    "limits": {
                      "applied_limit": null,
                      "truncated": false,
                      "continuation": null
                    },
                    "provenance": {
                      "signal": "metrics",
                      "schema_version": "1.0",
                      "query_ref": "qr_9f2c41a7b8d0e315"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "504": {
            "$ref": "#/components/responses/Timeout"
          }
        }
      }
    },
    "/api/v1/tenants/{tenant_id}/metrics/query_range": {
      "get": {
        "tags": [
          "Metrics"
        ],
        "summary": "Range metric query (MetricsQL)",
        "description": "Evaluate a MetricsQL/PromQL expression over a time range. Size `step` to your chart width (window ÷ pixels).\n\nThe window is clamped to the retention your plan keeps. `meta.effective_window` states the interval that was actually read and `clamped_by` says why it differs from the one you requested; `completeness.state` is `partial` when a clamp applied.",
        "operationId": "metricsQueryRange",
        "parameters": [
          {
            "$ref": "#/components/parameters/TenantId"
          },
          {
            "name": "query",
            "in": "query",
            "required": true,
            "description": "MetricsQL/PromQL expression.",
            "schema": {
              "type": "string"
            },
            "example": "histogram_quantile(0.95, sum(rate(http_request_duration_seconds_bucket[5m])) by (le))"
          },
          {
            "$ref": "#/components/parameters/Start"
          },
          {
            "$ref": "#/components/parameters/End"
          },
          {
            "name": "step",
            "in": "query",
            "required": false,
            "description": "Resolution step (e.g. `60s`).",
            "schema": {
              "type": "string",
              "default": "60s"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Range (matrix) result.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data"
                  ],
                  "properties": {
                    "data": {
                      "type": "object",
                      "description": "Prometheus-compatible result object."
                    },
                    "meta": {
                      "$ref": "#/components/schemas/ResultMeta"
                    }
                  }
                },
                "example": {
                  "data": {
                    "resultType": "matrix",
                    "result": [
                      {
                        "metric": {
                          "service": "api"
                        },
                        "values": [
                          [
                            1747219200,
                            "0.12"
                          ],
                          [
                            1747219260,
                            "0.14"
                          ]
                        ]
                      }
                    ]
                  },
                  "meta": {
                    "effective_window": {
                      "requested_start": "-90d",
                      "requested_end": "now",
                      "effective_start": "2026-05-07T10:00:00Z",
                      "effective_end": "2026-05-14T10:00:00Z",
                      "clamped_by": "retention"
                    },
                    "completeness": {
                      "state": "partial",
                      "reason": "retention_clamp"
                    },
                    "warnings": [
                      {
                        "severity": "warning",
                        "message": "The requested window was shortened to the retention available on this plan; see meta.effective_window.",
                        "source": "epok"
                      }
                    ],
                    "freshness": null,
                    "limits": {
                      "applied_limit": null,
                      "truncated": false,
                      "continuation": null
                    },
                    "provenance": {
                      "signal": "metrics",
                      "schema_version": "1.0",
                      "query_ref": "qr_9f2c41a7b8d0e315"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "504": {
            "$ref": "#/components/responses/Timeout"
          }
        }
      }
    },
    "/api/v1/tenants/{tenant_id}/metrics/labels": {
      "get": {
        "tags": [
          "Metrics"
        ],
        "summary": "List metric label names",
        "description": "Return the label names present in the tenant's metric store over an optional window.",
        "operationId": "metricsLabels",
        "parameters": [
          {
            "$ref": "#/components/parameters/TenantId"
          },
          {
            "name": "start",
            "in": "query",
            "required": false,
            "description": "Window start (ISO-8601 or relative). Omit to scan all retained data.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "end",
            "in": "query",
            "required": false,
            "description": "Window end.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Label names.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "labels": [
                    "__name__",
                    "service",
                    "instance",
                    "le"
                  ]
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "504": {
            "$ref": "#/components/responses/Timeout"
          }
        }
      }
    },
    "/api/v1/tenants/{tenant_id}/metrics/cardinality": {
      "get": {
        "tags": [
          "Metrics"
        ],
        "summary": "Metric cardinality breakdown",
        "description": "Top series-cardinality contributors — powers cardinality intelligence and cost attribution.",
        "operationId": "metricsCardinality",
        "parameters": [
          {
            "$ref": "#/components/parameters/TenantId"
          },
          {
            "name": "topn",
            "in": "query",
            "required": false,
            "description": "Number of top contributors (1-50).",
            "schema": {
              "type": "integer",
              "default": 20,
              "minimum": 1,
              "maximum": 50
            }
          },
          {
            "name": "focus_label",
            "in": "query",
            "required": false,
            "description": "Restrict the breakdown to one label.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Cardinality breakdown.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "504": {
            "$ref": "#/components/responses/Timeout"
          }
        }
      }
    },
    "/api/v1/tenants/{tenant_id}/traces": {
      "get": {
        "tags": [
          "Traces"
        ],
        "summary": "Search traces",
        "description": "Search distributed traces (OTLP spans, correlated by trace ID) and return per-trace summaries — service count, span count, error count, and duration.",
        "operationId": "searchTraces",
        "parameters": [
          {
            "$ref": "#/components/parameters/TenantId"
          },
          {
            "name": "query",
            "in": "query",
            "required": false,
            "description": "Filter over trace spans (Epok search syntax). Defaults to all traces.",
            "schema": {
              "type": "string",
              "default": "*"
            }
          },
          {
            "$ref": "#/components/parameters/Start"
          },
          {
            "$ref": "#/components/parameters/End"
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "Max traces to return (1-200).",
            "schema": {
              "type": "integer",
              "default": 50,
              "minimum": 1,
              "maximum": 200
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Trace summaries (newest first).",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "traces": [
                    {
                      "trace_id": "a1b2c3d4",
                      "services": [
                        "api",
                        "checkout",
                        "db"
                      ],
                      "span_count": 14,
                      "error_count": 1,
                      "start_time": "2026-05-14T10:00:00Z",
                      "duration_ms": 432
                    }
                  ],
                  "count": 1
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "504": {
            "$ref": "#/components/responses/Timeout"
          }
        }
      }
    },
    "/api/v1/tenants/{tenant_id}/traces/{trace_id}": {
      "get": {
        "tags": [
          "Traces"
        ],
        "summary": "Get a trace by ID",
        "description": "Return all spans for one trace ID, grouped by service for a swim-lane waterfall. Widens the time window automatically if the trace isn't in the requested window.",
        "operationId": "getTrace",
        "parameters": [
          {
            "$ref": "#/components/parameters/TenantId"
          },
          {
            "name": "trace_id",
            "in": "path",
            "required": true,
            "description": "Trace ID.",
            "schema": {
              "type": "string",
              "maxLength": 256
            }
          },
          {
            "$ref": "#/components/parameters/Start"
          },
          {
            "$ref": "#/components/parameters/End"
          }
        ],
        "responses": {
          "200": {
            "description": "Spans for the trace, grouped by service.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "504": {
            "$ref": "#/components/responses/Timeout"
          }
        }
      }
    },
    "/v1/rum/replay": {
      "post": {
        "tags": [
          "Ingest",
          "RUM"
        ],
        "summary": "Ingest a session-replay chunk",
        "description": "Upload one chunk of rrweb session-replay events (gzip accepted). Key-authed (ingest scope) — the browser SDK posts these. Browser web-vitals and errors are sent separately as OTLP metrics/traces. Returns 503 when session replay is not enabled for the tenant's plan.",
        "operationId": "ingestRumReplay",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object"
              },
              "example": {
                "session_id": "sess_abc123",
                "chunk_index": 0,
                "events": [
                  {
                    "type": 4,
                    "timestamp": 1747219200000,
                    "data": {}
                  }
                ]
              }
            }
          }
        },
        "responses": {
          "204": {
            "description": "Chunk stored."
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "413": {
            "description": "Replay chunk exceeds the size limit."
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "503": {
            "description": "Session replay is not enabled for this plan."
          }
        }
      }
    },
    "/api/v1/tenants/{tenant_id}/rum/sessions": {
      "get": {
        "tags": [
          "RUM"
        ],
        "summary": "List RUM sessions",
        "description": "List captured real-user (rrweb replay) sessions, newest first. Filter by errors, free text, and minimum duration.",
        "operationId": "listRumSessions",
        "parameters": [
          {
            "$ref": "#/components/parameters/TenantId"
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "Max sessions (default 50).",
            "schema": {
              "type": "integer",
              "default": 50,
              "minimum": 1,
              "maximum": 200
            }
          },
          {
            "name": "errors_only",
            "in": "query",
            "required": false,
            "description": "Only sessions that contained an error.",
            "schema": {
              "type": "boolean",
              "default": false
            }
          },
          {
            "name": "q",
            "in": "query",
            "required": false,
            "description": "Free-text filter (URL, user, etc.).",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "min_duration_ms",
            "in": "query",
            "required": false,
            "description": "Minimum session duration in milliseconds.",
            "schema": {
              "type": "integer",
              "default": 0,
              "minimum": 0
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Session summaries.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "sessions": [
                    {
                      "session_id": "sess_abc123",
                      "started_at": "2026-05-14T10:00:00Z",
                      "last_event_at": "2026-05-14T10:03:12Z",
                      "duration_ms": 192000,
                      "has_error": true,
                      "url": "/checkout"
                    }
                  ]
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/api/v1/tenants/{tenant_id}/rum/sessions/{session_id}": {
      "get": {
        "tags": [
          "RUM"
        ],
        "summary": "Get a RUM session",
        "description": "Session metadata plus the list of replay chunk indices (fetch chunk bytes separately to render the rrweb timeline).",
        "operationId": "getRumSession",
        "parameters": [
          {
            "$ref": "#/components/parameters/TenantId"
          },
          {
            "name": "session_id",
            "in": "path",
            "required": true,
            "description": "Session ID.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Session metadata + chunk index.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/api/v1/tenants/{tenant_id}/rum/sessions/by-trace/{trace_id}": {
      "get": {
        "tags": [
          "RUM"
        ],
        "summary": "Find the RUM session for a trace",
        "description": "Cross-signal pivot — given a backend trace ID, return the real-user session that produced it (when replay was captured). Powers trace <-> replay correlation.",
        "operationId": "getRumSessionByTrace",
        "parameters": [
          {
            "$ref": "#/components/parameters/TenantId"
          },
          {
            "name": "trace_id",
            "in": "path",
            "required": true,
            "description": "Backend trace ID.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The session correlated to this trace.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/api/v1/tenants/{tenant_id}/hits": {
      "get": {
        "tags": [
          "Search"
        ],
        "summary": "Volume histogram",
        "description": "Log count per time bucket for a query. Drives volume charts and anomaly visualizations.",
        "operationId": "getHits",
        "parameters": [
          {
            "$ref": "#/components/parameters/TenantId"
          },
          {
            "name": "query",
            "in": "query",
            "description": "Search filter (defaults to `*` — all logs).",
            "required": false,
            "schema": {
              "type": "string",
              "default": "*",
              "example": "level:error"
            }
          },
          {
            "$ref": "#/components/parameters/Start"
          },
          {
            "$ref": "#/components/parameters/End"
          },
          {
            "name": "step",
            "in": "query",
            "description": "Bucket size. One of `1m`, `5m`, `15m`, `1h`, `6h`, `1d`.",
            "required": false,
            "schema": {
              "type": "string",
              "enum": [
                "1m",
                "5m",
                "15m",
                "1h",
                "6h",
                "1d"
              ],
              "default": "1m"
            }
          },
          {
            "name": "fields_by",
            "in": "query",
            "description": "Group buckets by these fields (e.g. `_stream`, `service`). Max 10.",
            "required": false,
            "explode": true,
            "schema": {
              "type": "array",
              "items": {
                "type": "string"
              },
              "maxItems": 10
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Bucketed hit counts.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/HitsResponse"
                },
                "example": {
                  "hits": [
                    {
                      "time": "2026-05-14T09:55:00Z",
                      "hits": 340
                    },
                    {
                      "time": "2026-05-14T09:56:00Z",
                      "hits": 318
                    },
                    {
                      "time": "2026-05-14T09:57:00Z",
                      "hits": 1102
                    }
                  ],
                  "meta": {
                    "effective_window": {
                      "requested_start": "-1h",
                      "requested_end": "now",
                      "effective_start": "2026-05-14T09:00:00Z",
                      "effective_end": "2026-05-14T10:00:00Z",
                      "clamped_by": null
                    },
                    "completeness": {
                      "state": "complete",
                      "reason": null
                    },
                    "warnings": [],
                    "freshness": null,
                    "limits": {
                      "applied_limit": null,
                      "truncated": false,
                      "continuation": null
                    },
                    "provenance": {
                      "signal": "logs",
                      "schema_version": "1.0",
                      "query_ref": "qr_9f2c41a7b8d0e315"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "504": {
            "$ref": "#/components/responses/Timeout"
          }
        }
      }
    },
    "/api/v1/tenants/{tenant_id}/facets": {
      "get": {
        "tags": [
          "Search"
        ],
        "summary": "Field facets",
        "description": "Top values per field within a query window. Used to populate filter sidebars and discover what changed during an anomaly.",
        "operationId": "getFacets",
        "parameters": [
          {
            "$ref": "#/components/parameters/TenantId"
          },
          {
            "name": "query",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "default": "*",
              "example": "level:error"
            }
          },
          {
            "$ref": "#/components/parameters/Start"
          },
          {
            "$ref": "#/components/parameters/End"
          },
          {
            "name": "limit",
            "in": "query",
            "description": "Top-N values per field.",
            "required": false,
            "schema": {
              "type": "integer",
              "default": 20,
              "minimum": 1,
              "maximum": 1000
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Top values per field.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/FacetsResponse"
                },
                "example": {
                  "facets": {
                    "service": [
                      {
                        "value": "api",
                        "hits": 4012
                      },
                      {
                        "value": "worker",
                        "hits": 837
                      }
                    ],
                    "level": [
                      {
                        "value": "info",
                        "hits": 4621
                      },
                      {
                        "value": "error",
                        "hits": 228
                      }
                    ]
                  },
                  "truncated_fields": 0,
                  "meta": {
                    "effective_window": {
                      "requested_start": "-1h",
                      "requested_end": "now",
                      "effective_start": "2026-05-14T09:00:00Z",
                      "effective_end": "2026-05-14T10:00:00Z",
                      "clamped_by": null
                    },
                    "completeness": {
                      "state": "complete",
                      "reason": null
                    },
                    "warnings": [],
                    "freshness": null,
                    "limits": {
                      "applied_limit": 20,
                      "truncated": false,
                      "continuation": null
                    },
                    "provenance": {
                      "signal": "logs",
                      "schema_version": "1.0",
                      "query_ref": "qr_9f2c41a7b8d0e315"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "504": {
            "$ref": "#/components/responses/Timeout"
          }
        }
      }
    },
    "/api/v1/tenants/{tenant_id}/streams": {
      "get": {
        "tags": [
          "Search"
        ],
        "summary": "List streams",
        "description": "Streams (unique log sources, identified by their full label set) with hit counts in the query window.",
        "operationId": "getStreams",
        "parameters": [
          {
            "$ref": "#/components/parameters/TenantId"
          },
          {
            "name": "query",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "default": "*"
            }
          },
          {
            "$ref": "#/components/parameters/Start"
          },
          {
            "$ref": "#/components/parameters/End"
          }
        ],
        "responses": {
          "200": {
            "description": "Streams with hit counts.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/StreamsResponse"
                },
                "example": {
                  "streams": [
                    {
                      "id": "{service=\"api\",host=\"web-01\"}",
                      "hits": 3214
                    },
                    {
                      "id": "{service=\"worker\",host=\"queue-02\"}",
                      "hits": 891
                    }
                  ],
                  "meta": {
                    "effective_window": {
                      "requested_start": "-1h",
                      "requested_end": "now",
                      "effective_start": "2026-05-14T09:00:00Z",
                      "effective_end": "2026-05-14T10:00:00Z",
                      "clamped_by": null
                    },
                    "completeness": {
                      "state": "complete",
                      "reason": null
                    },
                    "warnings": [],
                    "freshness": null,
                    "limits": {
                      "applied_limit": 100,
                      "truncated": false,
                      "continuation": null
                    },
                    "provenance": {
                      "signal": "logs",
                      "schema_version": "1.0",
                      "query_ref": "qr_9f2c41a7b8d0e315"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/api/v1/tenants/{tenant_id}/alerts": {
      "get": {
        "tags": [
          "Alerts"
        ],
        "summary": "List alerts",
        "description": "Active (`firing`) and recently resolved alerts for the tenant, ordered newest first. Paginated.",
        "operationId": "listAlerts",
        "parameters": [
          {
            "$ref": "#/components/parameters/TenantId"
          },
          {
            "name": "state",
            "in": "query",
            "description": "Filter by lifecycle state.",
            "required": false,
            "schema": {
              "type": "string",
              "enum": [
                "firing",
                "resolved",
                "all"
              ]
            }
          },
          {
            "name": "severity",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "enum": [
                "info",
                "warning",
                "critical",
                "all"
              ]
            }
          },
          {
            "name": "detector_type",
            "in": "query",
            "description": "Filter by detector that produced the alert.",
            "required": false,
            "schema": {
              "type": "string",
              "example": "log_rate"
            }
          },
          {
            "name": "service",
            "in": "query",
            "description": "Filter to alerts whose stream contains `service=\"X\"`.",
            "required": false,
            "schema": {
              "type": "string",
              "maxLength": 512
            }
          },
          {
            "name": "since",
            "in": "query",
            "description": "Only return alerts fired after this ISO-8601 timestamp.",
            "required": false,
            "schema": {
              "type": "string",
              "format": "date-time"
            }
          },
          {
            "name": "until",
            "in": "query",
            "description": "Only return alerts fired before this ISO-8601 timestamp.",
            "required": false,
            "schema": {
              "type": "string",
              "format": "date-time"
            }
          },
          {
            "$ref": "#/components/parameters/Limit"
          },
          {
            "$ref": "#/components/parameters/Offset"
          }
        ],
        "responses": {
          "200": {
            "description": "Paginated alert list.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PaginatedAlerts"
                },
                "example": {
                  "items": [
                    {
                      "id": 18342,
                      "detector_type": "log_rate",
                      "stream": "{service=\"api\",host=\"web-01\"}",
                      "severity": "critical",
                      "title": "Log rate spike on api",
                      "description": "api logs spiked to 8.4× the seasonal baseline.",
                      "state": "firing",
                      "fired_at": "2026-05-14T10:02:11Z",
                      "fire_count": 3,
                      "evidence": {
                        "z_score": 8.4,
                        "current": 1102,
                        "baseline_mean": 131.2
                      }
                    }
                  ],
                  "total": 1,
                  "limit": 100,
                  "offset": 0,
                  "has_more": false
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/api/v1/tenants/{tenant_id}/alerts/{alert_id}": {
      "get": {
        "tags": [
          "Alerts"
        ],
        "summary": "Get alert",
        "operationId": "getAlert",
        "parameters": [
          {
            "$ref": "#/components/parameters/TenantId"
          },
          {
            "$ref": "#/components/parameters/AlertId"
          }
        ],
        "responses": {
          "200": {
            "description": "Alert.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Alert"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/api/v1/tenants/{tenant_id}/alerts/{alert_id}/resolve": {
      "post": {
        "tags": [
          "Alerts"
        ],
        "summary": "Manually resolve an alert",
        "description": "Marks the alert as resolved. Requires an API key with the `write` scope.",
        "operationId": "resolveAlert",
        "parameters": [
          {
            "$ref": "#/components/parameters/TenantId"
          },
          {
            "$ref": "#/components/parameters/AlertId"
          }
        ],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "note": {
                    "type": "string",
                    "description": "Optional resolution note."
                  }
                }
              },
              "example": {
                "note": "Cleared after deploy 2026.05.14-r2."
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Alert resolved.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Alert"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/api/v1/tenants/{tenant_id}/patterns": {
      "get": {
        "tags": [
          "Intelligence"
        ],
        "summary": "List log pattern clusters",
        "description": "Pattern clusters produced by the intelligence engine. Each cluster has a stable `fingerprint` that persists across log volume changes — same normalized template always returns the same fingerprint. Use this endpoint to discover new error types or surface trending patterns.",
        "operationId": "listPatterns",
        "parameters": [
          {
            "$ref": "#/components/parameters/TenantId"
          },
          {
            "name": "sort",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "enum": [
                "count",
                "first_seen",
                "last_seen"
              ],
              "default": "count"
            }
          },
          {
            "name": "order",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "enum": [
                "asc",
                "desc"
              ],
              "default": "desc"
            }
          },
          {
            "name": "since",
            "in": "query",
            "description": "Lookback window: `-1h`, `-24h`, `-7d`, etc.",
            "required": false,
            "schema": {
              "type": "string",
              "default": "-24h",
              "pattern": "^-\\d+[hd]$"
            }
          },
          {
            "name": "search",
            "in": "query",
            "description": "Substring filter on the normalized template.",
            "required": false,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "service",
            "in": "query",
            "description": "Filter to patterns whose streams contain `service=\"X\"`.",
            "required": false,
            "schema": {
              "type": "string",
              "maxLength": 512
            }
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "default": 50,
              "minimum": 1,
              "maximum": 2000
            }
          },
          {
            "$ref": "#/components/parameters/Offset"
          }
        ],
        "responses": {
          "200": {
            "description": "Pattern list.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PatternsResponse"
                },
                "example": {
                  "patterns": [
                    {
                      "fingerprint": "a1b2c3d4e5f6...",
                      "normalized_message": "connection timeout after <N>s on port <N>",
                      "example_raw_message": "connection timeout after 30s on port 5432",
                      "first_seen": "2026-05-14T10:02:14Z",
                      "last_seen": "2026-05-14T10:14:55Z",
                      "count": 1247,
                      "streams": [
                        "{service=\"api\",host=\"web-01\"}"
                      ]
                    }
                  ],
                  "has_more": false
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/ws/livetail/{tenant_id}": {
      "get": {
        "tags": [
          "Live"
        ],
        "summary": "WebSocket live-tail",
        "description": "Upgrade this endpoint to a WebSocket to stream matching log entries in real time. After the connection is open, send a JSON filter message `{\"query\": \"level:error\"}` to start the stream.\n\n**Auth note:** WebSocket auth uses the `Sec-WebSocket-Protocol` header with value `epok.bearer.<api_key>` (preferred) or a browser session cookie. Sending the API key as a query-string is supported for legacy clients but discouraged — query strings appear in server access logs.",
        "operationId": "wsLivetail",
        "parameters": [
          {
            "$ref": "#/components/parameters/TenantId"
          }
        ],
        "responses": {
          "101": {
            "description": "Protocol switched to WebSocket."
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        }
      }
    },
    "/ws/alerts/{tenant_id}": {
      "get": {
        "tags": [
          "Live"
        ],
        "summary": "WebSocket alert stream",
        "description": "Upgrade to a WebSocket to receive real-time alert fires and resolutions. Same auth options as the live-tail stream.",
        "operationId": "wsAlerts",
        "parameters": [
          {
            "$ref": "#/components/parameters/TenantId"
          }
        ],
        "responses": {
          "101": {
            "description": "Protocol switched to WebSocket."
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        }
      }
    },
    "/v1/traces": {
      "post": {
        "tags": [
          "Ingest"
        ],
        "summary": "OpenTelemetry HTTP traces (OTLP)",
        "description": "OpenTelemetry Protocol HTTP traces endpoint on `ingest.getepok.dev`. Use with the `otlphttp` exporter in the OpenTelemetry Collector or `OTEL_EXPORTER_OTLP_TRACES_ENDPOINT` on any OTEL SDK.\n\nAccepts both `application/json` (OTLP/HTTP JSON) and `application/x-protobuf` (OTLP/HTTP binary). Spans feed trace search, the service dependency graph, latency/error detectors, and cross-signal investigation.",
        "operationId": "ingestOtlpTraces",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object"
              }
            },
            "application/x-protobuf": {
              "schema": {
                "type": "string",
                "format": "binary"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Accepted."
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/v1/metrics": {
      "post": {
        "tags": [
          "Ingest"
        ],
        "summary": "OpenTelemetry HTTP metrics (OTLP JSON only)",
        "description": "OpenTelemetry Protocol HTTP metrics endpoint on `ingest.getepok.dev`.\n\n**Accepts `application/json` (OTLP/HTTP JSON) ONLY — protobuf is not supported on this endpoint.** In the OpenTelemetry Collector's `otlphttp` exporter you MUST set `encoding: json`, or metrics will be rejected while logs and traces work. Metrics feed the metric explorers and the metric anomaly/saturation/drift/reporting-gap detectors.",
        "operationId": "ingestOtlpMetrics",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Accepted (204-style empty success may also be returned)."
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/api/v1/tenants/{tenant_id}/search/unified": {
      "get": {
        "tags": [
          "Search"
        ],
        "summary": "Unified cross-signal search",
        "description": "One query across every signal: logs, backend spans, RUM events, metric events, metric NAMES, and session-replay sessions. Returns per-signal counts plus a small sample per section; each section degrades independently (one backend hiccup never blanks the rest). The recommended first call when investigating — it tells you which signal holds the evidence.",
        "operationId": "unifiedSearch",
        "parameters": [
          {
            "$ref": "#/components/parameters/TenantId"
          },
          {
            "name": "q",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Search query (same syntax as log search). Required."
          },
          {
            "name": "start",
            "in": "query",
            "schema": {
              "type": "string",
              "default": "-1h"
            },
            "description": "Window start: relative (`-5m`, `-1h`, `-7d`) or ISO8601."
          },
          {
            "name": "end",
            "in": "query",
            "schema": {
              "type": "string",
              "default": "now"
            },
            "description": "Window end: relative, `now`, or ISO8601."
          },
          {
            "name": "limit",
            "in": "query",
            "schema": {
              "type": "integer",
              "default": 5,
              "minimum": 1,
              "maximum": 20
            },
            "description": "Max items returned PER SECTION (logs, spans, rum, metric events, metric names, replay sessions)."
          }
        ],
        "responses": {
          "200": {
            "description": "Per-signal counts and samples.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "400": {
            "description": "Missing/empty `q`, or the query is invalid for all backends."
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/api/v1/tenants/{tenant_id}/spans/trace/{trace_id}": {
      "get": {
        "tags": [
          "Traces"
        ],
        "summary": "All spans in one trace",
        "description": "Every span recorded for a trace, for building a waterfall (assemble the tree with `parent_span_id`).\n\nLarge traces are paged. When more spans exist than a page holds, `meta.completeness` is `partial` with reason `span_cap` and `meta.limits.continuation` carries a cursor — pass it as `cursor`, together with the same `trace_id`, `start`, `end` and `limit`, for the next page.\n\nA continuation reads the interval its first page read, not a freshly resolved one. With the default `end=now` that matters: re-resolving the window between pages would let spans arriving mid-walk shift every later offset, so the walk would repeat or skip spans. Paging is therefore a snapshot of the trace as of the first page. Retention still applies — a cursor cannot read further back than your plan allows.\n\nA cursor is bound to the trace, the window you requested and the page size it came from; reusing it against a different query returns 400 rather than silently addressing the wrong rows.",
        "operationId": "getTraceSpans",
        "parameters": [
          {
            "$ref": "#/components/parameters/TenantId"
          },
          {
            "name": "trace_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "minLength": 16,
              "maxLength": 32
            },
            "example": "0123456789abcdef0123456789abcdef"
          },
          {
            "$ref": "#/components/parameters/Start"
          },
          {
            "$ref": "#/components/parameters/End"
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "Spans per page.",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 5000,
              "default": 2000
            }
          },
          {
            "name": "cursor",
            "in": "query",
            "required": false,
            "description": "Continuation token from a previous response's `meta.limits.continuation`.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Spans in this page, with the completeness of the page.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/TraceSpansResponse"
                },
                "example": {
                  "trace_id": "0123456789abcdef0123456789abcdef",
                  "spans": [
                    {
                      "_time": "2026-05-14T10:00:01.234Z",
                      "_msg": "POST /checkout",
                      "trace_id": "0123456789abcdef0123456789abcdef",
                      "span_id": "0123456789abcdef",
                      "service": "checkout",
                      "span_kind": "server",
                      "span_status": "error",
                      "duration_ms": "50.000",
                      "span_event_count": "2",
                      "span_events": [
                        {
                          "name": "exception",
                          "time": "2026-05-14T10:00:01.280Z",
                          "attributes": {
                            "exception.type": "TimeoutError"
                          }
                        },
                        {
                          "name": "retry",
                          "time": "2026-05-14T10:00:01.290Z",
                          "attributes": {
                            "attempt": "2"
                          }
                        }
                      ]
                    }
                  ],
                  "count": 2000,
                  "meta": {
                    "effective_window": {
                      "requested_start": "-1h",
                      "requested_end": "now",
                      "effective_start": "2026-05-14T09:00:00Z",
                      "effective_end": "2026-05-14T10:00:00Z",
                      "clamped_by": null
                    },
                    "completeness": {
                      "state": "partial",
                      "reason": "span_cap"
                    },
                    "warnings": [],
                    "freshness": null,
                    "limits": {
                      "applied_limit": 2000,
                      "truncated": true,
                      "continuation": "eyJ2IjoxLCJvIjoyMDAwfQ"
                    },
                    "provenance": {
                      "signal": "traces",
                      "schema_version": "1.0",
                      "query_ref": "qr_9f2c41a7b8d0e315"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "504": {
            "$ref": "#/components/responses/Timeout"
          }
        }
      }
    },
    "/api/v1/tenants/{tenant_id}/spans/search": {
      "get": {
        "tags": [
          "Traces"
        ],
        "summary": "Search traces",
        "description": "One row per matching trace. Without a `service` filter the rows are trace entry spans; with one they are that service's own spans at any depth, deduped to the slowest per trace — mid-tier services are rarely trace roots, so a root-only cohort would return nothing for them.\n\n`meta.completeness` is `partial` with reason `row_limit` when the result was cut — either because more spans matched than were read, or because grouping produced more traces than this page holds. `count` is the rows in this page, never the number of traces that matched.",
        "operationId": "searchSpans",
        "parameters": [
          {
            "$ref": "#/components/parameters/TenantId"
          },
          {
            "name": "query",
            "in": "query",
            "required": false,
            "description": "Additional search filter.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "service",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            },
            "example": "checkout"
          },
          {
            "name": "environment",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            },
            "example": "production"
          },
          {
            "name": "version",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            },
            "example": "v2.3.1"
          },
          {
            "name": "errors_only",
            "in": "query",
            "required": false,
            "schema": {
              "type": "boolean",
              "default": false
            }
          },
          {
            "name": "min_duration_ms",
            "in": "query",
            "required": false,
            "schema": {
              "type": "number"
            }
          },
          {
            "$ref": "#/components/parameters/Start"
          },
          {
            "$ref": "#/components/parameters/End"
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 1000,
              "default": 200
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Matching traces.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SpanSearchResponse"
                },
                "example": {
                  "traces": [
                    {
                      "_time": "2026-05-14T10:00:01.234Z",
                      "_msg": "POST /checkout",
                      "trace_id": "0123456789abcdef0123456789abcdef",
                      "span_id": "0123456789abcdef",
                      "service": "checkout",
                      "span_status": "error",
                      "duration_ms": "50.000"
                    }
                  ],
                  "count": 1,
                  "meta": {
                    "effective_window": {
                      "requested_start": "-1h",
                      "requested_end": "now",
                      "effective_start": "2026-05-14T09:00:00Z",
                      "effective_end": "2026-05-14T10:00:00Z",
                      "clamped_by": null
                    },
                    "completeness": {
                      "state": "complete",
                      "reason": null
                    },
                    "warnings": [],
                    "freshness": null,
                    "limits": {
                      "applied_limit": 200,
                      "truncated": false,
                      "continuation": null
                    },
                    "provenance": {
                      "signal": "traces",
                      "schema_version": "1.0",
                      "query_ref": "qr_9f2c41a7b8d0e315"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "504": {
            "$ref": "#/components/responses/Timeout"
          }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "BearerAuth": {
        "type": "http",
        "scheme": "bearer",
        "bearerFormat": "epk_<key>",
        "description": "Tenant-scoped API key. Required scope: `read` (default) or `write` (for state-changing operations)."
      },
      "BasicAuth": {
        "type": "http",
        "scheme": "basic",
        "description": "Username = API key, password = empty. Used by Loki-native shippers."
      },
      "ApiKeyHeader": {
        "type": "apiKey",
        "in": "header",
        "name": "X-API-Key",
        "description": "Legacy header form. Prefer `Authorization: Bearer`."
      }
    },
    "parameters": {
      "TenantId": {
        "name": "tenant_id",
        "in": "path",
        "required": true,
        "description": "Numeric tenant identifier. Must match the tenant the API key belongs to.",
        "schema": {
          "type": "integer",
          "minimum": 1,
          "example": 42
        }
      },
      "AlertId": {
        "name": "alert_id",
        "in": "path",
        "required": true,
        "schema": {
          "type": "integer",
          "minimum": 1,
          "example": 18342
        }
      },
      "Start": {
        "name": "start",
        "in": "query",
        "description": "Window start. Relative duration (`-1h`, `-15m`, `-7d`) or ISO-8601 timestamp.",
        "required": false,
        "schema": {
          "type": "string",
          "default": "-1h",
          "example": "-1h"
        }
      },
      "End": {
        "name": "end",
        "in": "query",
        "description": "Window end. Relative duration, ISO-8601 timestamp, or the literal `now`.",
        "required": false,
        "schema": {
          "type": "string",
          "default": "now",
          "example": "now"
        }
      },
      "Limit": {
        "name": "limit",
        "in": "query",
        "description": "Max results per page.",
        "required": false,
        "schema": {
          "type": "integer",
          "default": 100,
          "minimum": 1,
          "maximum": 2000
        }
      },
      "Offset": {
        "name": "offset",
        "in": "query",
        "description": "Zero-based pagination offset.",
        "required": false,
        "schema": {
          "type": "integer",
          "default": 0,
          "minimum": 0
        }
      }
    },
    "responses": {
      "BadRequest": {
        "description": "Request was malformed (invalid syntax, parameter out of range, etc.).",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ErrorResponse"
            },
            "example": {
              "detail": "Query cannot be empty",
              "request_id": "3f0e352efc32"
            }
          }
        }
      },
      "Unauthorized": {
        "description": "Missing or invalid API key.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ErrorResponse"
            },
            "example": {
              "detail": "Unauthorized",
              "request_id": "3f0e352efc32"
            }
          }
        }
      },
      "Forbidden": {
        "description": "API key lacks the required scope (e.g. `write`), or this is a read-only public demo tenant.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ErrorResponse"
            },
            "example": {
              "detail": "API key missing required 'write' scope",
              "request_id": "3f0e352efc32"
            }
          }
        }
      },
      "NotFound": {
        "description": "Resource does not exist or is not visible to this tenant.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ErrorResponse"
            },
            "example": {
              "detail": "Not Found",
              "request_id": "3f0e352efc32"
            }
          }
        }
      },
      "PayloadTooLarge": {
        "description": "Ingest body exceeded the per-request size cap.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ErrorResponse"
            },
            "example": {
              "detail": "Payload too large",
              "request_id": "3f0e352efc32"
            }
          }
        }
      },
      "RateLimited": {
        "description": "Per-tenant or per-key rate limit exceeded. Inspect the `Retry-After` response header (seconds) before retrying.",
        "headers": {
          "Retry-After": {
            "description": "Seconds to wait before retrying.",
            "schema": {
              "type": "integer",
              "example": 30
            }
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ErrorResponse"
            },
            "example": {
              "detail": "Rate limit exceeded",
              "request_id": "3f0e352efc32"
            }
          }
        }
      },
      "Timeout": {
        "description": "Log backend query timed out. Try narrowing the time range or simplifying the query.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ErrorResponse"
            },
            "example": {
              "detail": "Log query timed out",
              "request_id": "3f0e352efc32"
            }
          }
        }
      }
    },
    "schemas": {
      "HealthResponse": {
        "type": "object",
        "required": [
          "status"
        ],
        "properties": {
          "status": {
            "type": "string",
            "enum": [
              "ok"
            ],
            "example": "ok"
          }
        }
      },
      "ErrorResponse": {
        "type": "object",
        "required": [
          "detail"
        ],
        "properties": {
          "detail": {
            "type": "string",
            "description": "Human-readable error message.",
            "example": "Query cannot be empty"
          },
          "request_id": {
            "type": "string",
            "description": "Echoes the `X-Request-Id` response header. Include this when contacting support.",
            "example": "3f0e352efc32"
          }
        }
      },
      "IngestAck": {
        "type": "object",
        "properties": {
          "took": {
            "type": "integer",
            "description": "Processing time in milliseconds.",
            "example": 4
          },
          "errors": {
            "type": "boolean",
            "description": "True if any entry was rejected.",
            "example": false
          },
          "items": {
            "type": "array",
            "description": "Per-entry status (empty when `errors=false`).",
            "items": {
              "type": "object"
            }
          }
        }
      },
      "LokiPushBody": {
        "type": "object",
        "required": [
          "streams"
        ],
        "properties": {
          "streams": {
            "type": "array",
            "items": {
              "type": "object",
              "required": [
                "stream",
                "values"
              ],
              "properties": {
                "stream": {
                  "type": "object",
                  "description": "Label set for this stream. Free-form keys/values.",
                  "additionalProperties": {
                    "type": "string"
                  },
                  "example": {
                    "service": "api",
                    "host": "web-01"
                  }
                },
                "values": {
                  "type": "array",
                  "description": "Array of `[unix_ns_timestamp_string, log_line]` pairs.",
                  "items": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    },
                    "minItems": 2,
                    "maxItems": 2
                  }
                }
              }
            }
          }
        }
      },
      "SearchRequest": {
        "type": "object",
        "required": [
          "query"
        ],
        "properties": {
          "query": {
            "type": "string",
            "description": "Search query. Max 10,000 characters.",
            "maxLength": 10000,
            "example": "level:error service:api"
          },
          "start": {
            "type": "string",
            "description": "Window start. Relative (`-1h`) or ISO-8601.",
            "default": "-1h",
            "example": "-1h"
          },
          "end": {
            "type": "string",
            "description": "Window end.",
            "default": "now",
            "example": "now"
          },
          "limit": {
            "type": "integer",
            "description": "Max log entries to return.",
            "default": 1000,
            "minimum": 1,
            "maximum": 10000
          },
          "before": {
            "type": "string",
            "description": "Cursor: when paginating, pass the previous response's `oldest_time` to fetch older entries.",
            "format": "date-time",
            "nullable": true
          }
        }
      },
      "SearchResponse": {
        "type": "object",
        "required": [
          "logs",
          "count"
        ],
        "properties": {
          "logs": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/LogEntry"
            }
          },
          "count": {
            "type": "integer",
            "description": "Number of entries in this response.",
            "example": 1
          },
          "elapsed_ms": {
            "type": "integer",
            "description": "Server-side query time.",
            "example": 142
          },
          "has_more": {
            "type": "boolean",
            "description": "True when more entries exist before `oldest_time`. Use `before` to paginate.",
            "example": false
          },
          "oldest_time": {
            "type": "string",
            "format": "date-time",
            "nullable": true,
            "example": "2026-05-14T10:00:01.234Z"
          },
          "meta": {
            "$ref": "#/components/schemas/ResultMeta"
          }
        }
      },
      "LogEntry": {
        "type": "object",
        "description": "Single log entry. Schemaless — `_time`, `_msg`, `_stream` are always present; other fields depend on what the producer sent.",
        "required": [
          "_time",
          "_msg"
        ],
        "properties": {
          "_time": {
            "type": "string",
            "format": "date-time",
            "example": "2026-05-14T10:00:01.234Z"
          },
          "_msg": {
            "type": "string",
            "example": "GET /api/users 200 42ms"
          },
          "_stream": {
            "type": "string",
            "example": "{service=\"api\",host=\"web-01\"}"
          },
          "level": {
            "type": "string",
            "example": "info"
          },
          "service": {
            "type": "string",
            "example": "api"
          }
        },
        "additionalProperties": true
      },
      "HitsResponse": {
        "type": "object",
        "required": [
          "hits"
        ],
        "properties": {
          "hits": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/HitsBucket"
            }
          },
          "meta": {
            "$ref": "#/components/schemas/ResultMeta"
          }
        }
      },
      "HitsBucket": {
        "type": "object",
        "required": [
          "time",
          "hits"
        ],
        "properties": {
          "time": {
            "type": "string",
            "format": "date-time",
            "example": "2026-05-14T09:55:00Z"
          },
          "hits": {
            "type": "integer",
            "minimum": 0,
            "example": 340
          },
          "stream": {
            "type": "string",
            "nullable": true,
            "description": "Present when grouped via `fields_by=_stream`."
          }
        },
        "additionalProperties": true
      },
      "FacetsResponse": {
        "type": "object",
        "required": [
          "facets"
        ],
        "properties": {
          "facets": {
            "type": "object",
            "additionalProperties": {
              "type": "array",
              "items": {
                "$ref": "#/components/schemas/FacetValue"
              }
            }
          },
          "truncated_fields": {
            "type": "integer",
            "description": "Number of fields omitted from the response because the per-request cap was reached.",
            "example": 0
          },
          "meta": {
            "$ref": "#/components/schemas/ResultMeta"
          }
        }
      },
      "FacetValue": {
        "type": "object",
        "required": [
          "value",
          "hits"
        ],
        "properties": {
          "value": {
            "type": "string",
            "example": "api"
          },
          "hits": {
            "type": "integer",
            "example": 4012
          }
        }
      },
      "StreamsResponse": {
        "type": "object",
        "required": [
          "streams"
        ],
        "properties": {
          "streams": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/StreamInfo"
            }
          },
          "meta": {
            "$ref": "#/components/schemas/ResultMeta"
          }
        }
      },
      "StreamInfo": {
        "type": "object",
        "required": [
          "id",
          "hits"
        ],
        "properties": {
          "id": {
            "type": "string",
            "description": "Stream label set in `{k=\"v\",…}` form.",
            "example": "{service=\"api\",host=\"web-01\"}"
          },
          "hits": {
            "type": "integer",
            "example": 3214
          }
        }
      },
      "Alert": {
        "type": "object",
        "required": [
          "id",
          "detector_type",
          "stream",
          "severity",
          "title",
          "description",
          "state",
          "fired_at",
          "fire_count",
          "evidence"
        ],
        "properties": {
          "id": {
            "type": "integer",
            "example": 18342
          },
          "detector_type": {
            "type": "string",
            "description": "Which detector produced this alert.",
            "example": "log_rate"
          },
          "stream": {
            "type": "string",
            "example": "{service=\"api\",host=\"web-01\"}"
          },
          "severity": {
            "type": "string",
            "enum": [
              "info",
              "warning",
              "critical"
            ],
            "example": "critical"
          },
          "title": {
            "type": "string",
            "example": "Log rate spike on api"
          },
          "description": {
            "type": "string",
            "example": "api logs spiked to 8.4× the seasonal baseline."
          },
          "evidence": {
            "type": "object",
            "description": "Detector-specific evidence (z-score, sample messages, etc.).",
            "additionalProperties": true,
            "example": {
              "z_score": 8.4,
              "current": 1102,
              "baseline_mean": 131.2
            }
          },
          "state": {
            "type": "string",
            "enum": [
              "firing",
              "resolved"
            ],
            "example": "firing"
          },
          "fired_at": {
            "type": "string",
            "format": "date-time",
            "example": "2026-05-14T10:02:11Z"
          },
          "resolved_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          },
          "fire_count": {
            "type": "integer",
            "description": "How many times this dedup key has re-fired.",
            "example": 3
          },
          "last_updated_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          },
          "resolution_note": {
            "type": "string",
            "nullable": true
          },
          "incident_id": {
            "type": "integer",
            "nullable": true,
            "description": "Incident this alert belongs to, if grouped."
          }
        }
      },
      "PaginatedAlerts": {
        "type": "object",
        "required": [
          "items",
          "total",
          "limit",
          "offset",
          "has_more"
        ],
        "properties": {
          "items": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Alert"
            }
          },
          "total": {
            "type": "integer",
            "minimum": 0,
            "example": 1
          },
          "limit": {
            "type": "integer",
            "example": 100
          },
          "offset": {
            "type": "integer",
            "example": 0
          },
          "has_more": {
            "type": "boolean",
            "example": false
          }
        }
      },
      "PatternsResponse": {
        "type": "object",
        "required": [
          "patterns"
        ],
        "properties": {
          "patterns": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Pattern"
            }
          },
          "has_more": {
            "type": "boolean",
            "example": false
          }
        }
      },
      "Pattern": {
        "type": "object",
        "required": [
          "fingerprint",
          "normalized_message",
          "count",
          "first_seen",
          "last_seen"
        ],
        "properties": {
          "fingerprint": {
            "type": "string",
            "description": "Stable SHA-256 hex hash of the normalized template.",
            "example": "a1b2c3d4e5f6..."
          },
          "normalized_message": {
            "type": "string",
            "description": "Template with numbers, IPs, and IDs replaced with placeholders.",
            "example": "connection timeout after <N>s on port <N>"
          },
          "example_raw_message": {
            "type": "string",
            "description": "One concrete message that matches this pattern.",
            "example": "connection timeout after 30s on port 5432"
          },
          "first_seen": {
            "type": "string",
            "format": "date-time",
            "example": "2026-05-14T10:02:14Z"
          },
          "last_seen": {
            "type": "string",
            "format": "date-time",
            "example": "2026-05-14T10:14:55Z"
          },
          "count": {
            "type": "integer",
            "minimum": 0,
            "example": 1247
          },
          "streams": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Stream selectors this pattern has appeared in.",
            "example": [
              "{service=\"api\",host=\"web-01\"}"
            ]
          }
        }
      },
      "EffectiveWindow": {
        "type": "object",
        "description": "The window you asked for next to the window that was actually read. When `clamped_by` is set, the result covers `effective_start`..`effective_end`, not the requested range.",
        "required": [
          "requested_start",
          "requested_end",
          "effective_start",
          "effective_end",
          "clamped_by"
        ],
        "properties": {
          "requested_start": {
            "type": "string",
            "nullable": true,
            "description": "Exactly what you sent, relative or absolute.",
            "example": "-30d"
          },
          "requested_end": {
            "type": "string",
            "nullable": true,
            "example": "now"
          },
          "effective_start": {
            "type": "string",
            "nullable": true,
            "description": "Absolute start of the window that was read.",
            "example": "2026-05-07T10:00:00Z"
          },
          "effective_end": {
            "type": "string",
            "nullable": true,
            "example": "2026-05-14T10:00:00Z"
          },
          "clamped_by": {
            "type": "string",
            "nullable": true,
            "enum": [
              "retention",
              "max_range",
              null
            ],
            "description": "Why the window moved. `retention` = the requested start is older than your plan keeps data.",
            "example": "retention"
          }
        }
      },
      "Completeness": {
        "type": "object",
        "description": "Whether this result is the whole answer to the question asked.\n\n`complete` means the query read everything retained that matched — it is NOT a claim that everything that happened was captured. Capture completeness is a separate question and stays `unknown`.",
        "required": [
          "state",
          "reason"
        ],
        "properties": {
          "state": {
            "type": "string",
            "enum": [
              "complete",
              "partial",
              "sampled",
              "stale",
              "unavailable",
              "unknown"
            ],
            "example": "partial"
          },
          "reason": {
            "type": "string",
            "nullable": true,
            "description": "Null when complete. Otherwise what cut the result: `span_cap`, `row_limit`, `field_cap`, `stream_cap`, `retention_clamp`, `adaptive_window`, `signal_unavailable`, `no_span_metrics`, `span_retrieval_ceiling`.",
            "example": "span_cap"
          }
        }
      },
      "ResultWarning": {
        "type": "object",
        "description": "One annotation about this result — either raised by Epok or passed through from the underlying store.",
        "required": [
          "severity",
          "message",
          "source"
        ],
        "properties": {
          "severity": {
            "type": "string",
            "enum": [
              "warning",
              "info"
            ],
            "example": "warning"
          },
          "message": {
            "type": "string",
            "example": "incomplete source coverage"
          },
          "source": {
            "type": "string",
            "enum": [
              "epok",
              "log_store",
              "metrics_store"
            ],
            "example": "metrics_store"
          }
        }
      },
      "ResultLimits": {
        "type": "object",
        "required": [
          "applied_limit",
          "truncated",
          "continuation"
        ],
        "properties": {
          "applied_limit": {
            "type": "integer",
            "nullable": true,
            "description": "The row/field cap that was in force.",
            "example": 2000
          },
          "truncated": {
            "type": "boolean",
            "description": "True when the cap cut the result.",
            "example": true
          },
          "continuation": {
            "type": "string",
            "nullable": true,
            "description": "Opaque token for the next page. Pass it back in `cursor` (or, for log search, as `before`). Null when there is no next page or the result cannot be paged (aggregations).",
            "example": "eyJ2IjoxLCJvIjoyMDAwfQ"
          }
        }
      },
      "ResultProvenance": {
        "type": "object",
        "required": [
          "signal",
          "schema_version",
          "query_ref"
        ],
        "properties": {
          "signal": {
            "type": "string",
            "enum": [
              "logs",
              "metrics",
              "traces",
              "cross_signal"
            ],
            "example": "traces"
          },
          "schema_version": {
            "type": "string",
            "description": "Version of the `meta` contract itself.",
            "example": "1.0"
          },
          "query_ref": {
            "type": "string",
            "nullable": true,
            "description": "Stable digest of the executed query, scope and window. The same question always yields the same reference, so two results can be compared or an evidence trail replayed.",
            "example": "qr_9f2c41a7b8d0e315"
          }
        }
      },
      "ResultMeta": {
        "type": "object",
        "description": "The query-result contract, returned by every supported read endpoint.\n\nIt exists so a caller — a person or an agent — can tell a whole answer from a cut one. Read `completeness.state` before you act on a result, and `effective_window` before you compare two.\n\n`meta` is additive: every other field of the response keeps its existing shape, so clients written before this contract keep working.",
        "required": [
          "effective_window",
          "completeness",
          "warnings",
          "freshness",
          "limits",
          "provenance"
        ],
        "properties": {
          "effective_window": {
            "$ref": "#/components/schemas/EffectiveWindow"
          },
          "completeness": {
            "$ref": "#/components/schemas/Completeness"
          },
          "warnings": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/ResultWarning"
            }
          },
          "freshness": {
            "type": "string",
            "nullable": true,
            "description": "Dataset watermark, when one is available. Null otherwise — it is never inferred from the result's own newest row, which would always look fresh."
          },
          "limits": {
            "$ref": "#/components/schemas/ResultLimits"
          },
          "provenance": {
            "$ref": "#/components/schemas/ResultProvenance"
          }
        }
      },
      "TraceSpansResponse": {
        "type": "object",
        "required": [
          "trace_id",
          "spans",
          "count"
        ],
        "properties": {
          "trace_id": {
            "type": "string",
            "example": "0123456789abcdef0123456789abcdef"
          },
          "spans": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Span"
            }
          },
          "count": {
            "type": "integer",
            "description": "Spans in THIS page. Check `meta.completeness` before treating it as the size of the trace.",
            "example": 2000
          },
          "meta": {
            "$ref": "#/components/schemas/ResultMeta"
          }
        }
      },
      "Span": {
        "type": "object",
        "description": "One span. Schemaless beyond the fields below — every attribute your instrumentation set is present as a top-level key.",
        "properties": {
          "_time": {
            "type": "string",
            "format": "date-time"
          },
          "_msg": {
            "type": "string",
            "description": "Span name.",
            "example": "POST /checkout"
          },
          "trace_id": {
            "type": "string"
          },
          "span_id": {
            "type": "string"
          },
          "parent_span_id": {
            "type": "string",
            "description": "Absent on a trace's entry span."
          },
          "service": {
            "type": "string",
            "example": "checkout"
          },
          "operation": {
            "type": "string",
            "example": "POST /checkout"
          },
          "span_kind": {
            "type": "string",
            "enum": [
              "server",
              "client",
              "producer",
              "consumer",
              "internal",
              "unspecified"
            ]
          },
          "span_status": {
            "type": "string",
            "enum": [
              "ok",
              "error",
              "unset"
            ]
          },
          "duration_ms": {
            "type": "string",
            "example": "50.000"
          },
          "span_events": {
            "type": "array",
            "description": "EVERY event recorded on the span, in order — not only the first exception. Each carries `name`, `time` and its original attribute keys.",
            "items": {
              "type": "object",
              "properties": {
                "name": {
                  "type": "string",
                  "example": "exception"
                },
                "time": {
                  "type": "string",
                  "format": "date-time"
                },
                "attributes": {
                  "type": "object"
                }
              }
            }
          },
          "span_event_count": {
            "type": "string",
            "description": "Total events on the span. Larger than `span_events` when the per-span payload cap bit.",
            "example": "3"
          },
          "span_links": {
            "type": "array",
            "description": "Links to other spans — the record of asynchronous causality (a queue consumer's tie back to its producer).",
            "items": {
              "type": "object",
              "properties": {
                "trace_id": {
                  "type": "string"
                },
                "span_id": {
                  "type": "string"
                },
                "trace_state": {
                  "type": "string"
                },
                "attributes": {
                  "type": "object"
                }
              }
            }
          },
          "span_link_count": {
            "type": "string",
            "example": "1"
          },
          "span_dropped_events_count": {
            "type": "string",
            "description": "Events your SDK discarded before sending. Present only when non-zero."
          },
          "span_dropped_links_count": {
            "type": "string"
          },
          "exception_type": {
            "type": "string",
            "description": "First exception event's type, promoted for search. A convenience index over `span_events`."
          },
          "exception_message": {
            "type": "string"
          },
          "exception_count": {
            "type": "string"
          }
        }
      },
      "SpanSearchResponse": {
        "type": "object",
        "required": [
          "traces",
          "count"
        ],
        "properties": {
          "traces": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Span"
            },
            "description": "One representative span per trace."
          },
          "count": {
            "type": "integer",
            "example": 42
          },
          "meta": {
            "$ref": "#/components/schemas/ResultMeta"
          }
        }
      }
    }
  }
}
