{ "components": { "schemas": { "AppError": { "oneOf": [ { "example": "Internal Server Error", "properties": { "InternalServerError": { "type": "string" } }, "required": [ "InternalServerError" ], "type": "object" }, { "example": "Bad Request", "properties": { "BadRequest": { "type": "string" } }, "required": [ "BadRequest" ], "type": "object" }, { "example": "Not Found", "properties": { "NotFound": { "type": "string" } }, "required": [ "NotFound" ], "type": "object" }, { "example": "Unauthorized", "properties": { "Unauthorized": { "type": "string" } }, "required": [ "Unauthorized" ], "type": "object" }, { "example": "Forbidden", "properties": { "Forbidden": { "type": "string" } }, "required": [ "Forbidden" ], "type": "object" }, { "example": "Storage Error", "properties": { "Storage": { "type": "string" } }, "required": [ "Storage" ], "type": "object" } ] }, "HealthResponse": { "properties": { "status": { "type": "string" } }, "required": [ "status" ], "type": "object" }, "ReadinessResponse": { "properties": { "database": { "type": "string" }, "error": { "type": [ "string", "null" ] }, "status": { "type": "string" } }, "required": [ "status", "database" ], "type": "object" }, "VacuumResponse": { "description": "What a vacuum did.", "properties": { "duplicates_removed": { "description": "Number of duplicate samples removed (same series, same timestamp, same value, the first\none written is kept), or `null` when the storage backend cannot remove duplicates", "format": "int64", "minimum": 0, "type": [ "integer", "null" ] }, "status": { "description": "`ok` when the maintenance completed", "type": "string" } }, "required": [ "status" ], "type": "object" } } }, "info": { "description": "Sensor data ingestion, storage, and retrieval platform", "license": { "identifier": "Apache-2.0", "name": "Apache-2.0" }, "title": "sensapp", "version": "0.3.0" }, "openapi": "3.1.0", "paths": { "/": { "get": { "operationId": "frontpage", "responses": { "200": { "content": { "text/plain": { "schema": { "type": "string" } } }, "description": "SensApp Frontpage: the name of the instance. Redirects to the web UI at `/ui/` instead when the UI is served" } }, "tags": [ "SensApp" ] } }, "/api/v1/admin/vacuum": { "post": { "description": "Removes the duplicate samples (a retried write, a client that sends twice, a crash in the middle\nof a request leave some), then cleans up and optimizes the database and reclaims space, as far as\nthe storage backend supports it. Only exact duplicates go: two different values at the same\ntimestamp are both kept. Requires the `delete` scope, and a token without a sensor allow list.", "operationId": "vacuum_database", "responses": { "200": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/VacuumResponse" } } }, "description": "Maintenance completed, with the number of duplicate samples removed" }, "403": { "description": "The token does not have the delete scope, or has a sensor allow list" }, "500": { "content": { "text/plain": { "schema": { "type": "string" } } }, "description": "Failed to vacuum database" }, "503": { "description": "The storage backend is unavailable: retry later" }, "504": { "description": "The request took longer than SENSAPP_HTTP_MAINTENANCE_TIMEOUT_SECONDS (one hour by default), and the storage backend may carry on after the answer" } }, "summary": "Database Vacuuming", "tags": [ "Admin" ] } }, "/api/v1/prometheus_remote_read": { "post": { "description": "Allows you to read data from SensApp using Prometheus remote read protocol.\n\nIt follows the [Prometheus Remote Read specification](https://prometheus.io/docs/prometheus/latest/querying/remote_read_api/).", "operationId": "prometheus_remote_read", "parameters": [ { "description": "Content encoding, must be snappy", "in": "header", "name": "content-encoding", "required": true, "schema": { "format": "snappy", "type": "string" } }, { "description": "Content type, must be application/x-protobuf", "in": "header", "name": "content-type", "required": true, "schema": { "format": "application/x-protobuf", "type": "string" } }, { "description": "Prometheus Remote Read version, must be 0.1.0", "in": "header", "name": "x-prometheus-remote-read-version", "required": true, "schema": { "format": "0.1.0", "type": "string" } } ], "requestBody": { "content": { "application/x-protobuf": {} }, "description": "Prometheus Remote Read endpoint. [Reference](https://prometheus.io/docs/prometheus/latest/querying/remote_read_api/)" }, "responses": { "200": { "content": { "application/x-protobuf": {} }, "description": "Read Response" }, "400": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/AppError" } } }, "description": "Bad Request" }, "500": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/AppError" } } }, "description": "Internal Server Error" }, "503": { "description": "The storage backend is unavailable: retry later" }, "504": { "description": "The request took longer than SENSAPP_HTTP_SERVER_TIMEOUT_SECONDS, usually because the storage backend hangs" } }, "summary": "Prometheus Remote Read API.", "tags": [ "Prometheus" ] } }, "/api/v1/prometheus_remote_write": { "post": { "description": "Allows you to write data from Prometheus to SensApp.\n\nIt follows the [Prometheus Remote Write specification](https://prometheus.io/docs/concepts/remote_write_spec/).", "operationId": "publish_prometheus", "parameters": [ { "description": "Content encoding, must be snappy", "in": "header", "name": "content-encoding", "required": true, "schema": { "format": "snappy", "type": "string" } }, { "description": "Content type, must be application/x-protobuf", "in": "header", "name": "content-type", "required": true, "schema": { "format": "application/x-protobuf", "type": "string" } }, { "description": "Prometheus Remote Write version, must be 0.1.0", "in": "header", "name": "x-prometheus-remote-write-version", "required": true, "schema": { "format": "0.1.0", "type": "string" } } ], "requestBody": { "content": { "application/x-protobuf": {} }, "description": "Prometheus Remote Write endpoint. [Reference](https://prometheus.io/docs/concepts/remote_write_spec/)" }, "responses": { "204": { "description": "No Content" }, "400": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/AppError" } } }, "description": "Bad Request" }, "500": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/AppError" } } }, "description": "Internal Server Error" }, "503": { "description": "The storage backend is unavailable, or SensApp is busy writing and sheds the write. A `Retry-After` header (seconds, randomised) means the request was not processed at all and can be sent again after that delay", "headers": { "Retry-After": { "description": "Seconds to wait before sending the write again", "schema": { "format": "int32", "minimum": 0, "type": "integer" } } } }, "504": { "description": "The request took longer than SENSAPP_HTTP_SERVER_TIMEOUT_SECONDS, usually because the storage backend hangs" } }, "summary": "Prometheus Remote Write API.", "tags": [ "Prometheus" ] } }, "/api/v1/query": { "get": { "description": "Parses a PromQL expression and returns matching time series data.\nOnly simple selectors (VectorSelector and MatrixSelector) are supported.\n\n# Examples\n\n- Simple metric: `GET /api/v1/query?query=my_metric`\n- With labels: `GET /api/v1/query?query=my_metric{env=\"prod\"}`\n- Range query: `GET /api/v1/query?query=my_metric[5m]`\n- With format: `GET /api/v1/query?query=my_metric&format=csv`", "operationId": "simple_promql_query", "parameters": [ { "description": "PromQL query string (e.g., 'my_metric{label=\"value\"}' or 'my_metric[5m]')", "in": "query", "name": "query", "required": true, "schema": { "type": "string" } }, { "description": "Output format: senml (default), csv, jsonl, or arrow", "in": "query", "name": "format", "required": false, "schema": { "type": "string" } }, { "description": "Bucket width for aggregation queries, using Prometheus duration syntax (e.g., '5m'). Without it, the whole window is one bucket", "in": "query", "name": "step", "required": false, "schema": { "type": "string" } } ], "responses": { "200": { "content": { "application/json": { "schema": {} } }, "description": "Query results in requested format" }, "400": { "description": "Invalid or unsupported PromQL query" }, "500": { "description": "Internal server error" }, "503": { "description": "The storage backend is unavailable: retry later" }, "504": { "description": "The request took longer than SENSAPP_HTTP_SERVER_TIMEOUT_SECONDS, usually because the storage backend hangs" } }, "summary": "Simple PromQL query endpoint.", "tags": [ "SensApp" ] } }, "/api/v2/write": { "post": { "description": "Allows you to write data from InfluxDB or Telegraf to SensApp.\n[More information.](https://github.com/SINTEF/sensapp/blob/main/docs/INFLUX_DB.md)", "operationId": "publish_influxdb", "parameters": [ { "description": "Bucket name", "example": "sensapp", "in": "query", "name": "bucket", "required": true, "schema": { "type": "string" } }, { "description": "Organization name", "example": "sensapp", "in": "query", "name": "org", "required": false, "schema": { "type": "string" } }, { "description": "Organization ID", "in": "query", "name": "org_id", "required": false, "schema": { "type": "string" } }, { "description": "Precision of the timestamps. One of ns, us, ms, s", "in": "query", "name": "precision", "required": false, "schema": { "type": "string" } } ], "requestBody": { "content": { "text/plain": { "example": "cpu,host=A,region=west usage_system=64.2 1590488773254420000", "schema": { "type": "string" } } }, "description": "InfluxDB Line Protocol endpoint. [Reference](https://docs.influxdata.com/influxdb/v2/reference/syntax/line-protocol/).", "required": true }, "responses": { "204": { "description": "No Content" }, "400": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/AppError" } } }, "description": "Bad Request" }, "500": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/AppError" } } }, "description": "Internal Server Error" }, "503": { "description": "The storage backend is unavailable, or SensApp is busy writing and sheds the write. A `Retry-After` header (seconds, randomised) means the request was not processed at all and can be sent again after that delay", "headers": { "Retry-After": { "description": "Seconds to wait before sending the write again", "schema": { "format": "int32", "minimum": 0, "type": "integer" } } } }, "504": { "description": "The request took longer than SENSAPP_HTTP_SERVER_TIMEOUT_SECONDS, usually because the storage backend hangs" } }, "summary": "InfluxDB Compatible Write API.", "tags": [ "InfluxDB" ] } }, "/health/live": { "get": { "description": "Checks if the service is running.\nThis endpoint always returns 200 OK if the server is able to respond.", "operationId": "liveness", "responses": { "200": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/HealthResponse" } } }, "description": "Service is alive" } }, "summary": "Liveness check", "tags": [ "Health" ] } }, "/health/ready": { "get": { "description": "Checks if the service is ready to accept traffic.\nThis includes checking if the database connection is working", "operationId": "readiness", "responses": { "200": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ReadinessResponse" } } }, "description": "Service is ready" }, "503": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ReadinessResponse" } } }, "description": "Service is not ready" } }, "summary": "Readiness check", "tags": [ "Health" ] } }, "/metrics": { "get": { "operationId": "list_metrics", "parameters": [ { "description": "Filter metrics by name (substring match)", "in": "query", "name": "name", "required": false, "schema": { "type": "string" } }, { "description": "Filter metrics by name using regex pattern", "in": "query", "name": "name_regex", "required": false, "schema": { "type": "string" } }, { "description": "Filter metrics by sensor type (float, integer, string, boolean, location, json, blob, numeric)", "in": "query", "name": "type", "required": false, "schema": { "type": "string" } } ], "responses": { "200": { "content": { "application/json": { "schema": {} } }, "description": "Metrics catalog in DCAT format" }, "503": { "description": "The storage backend is unavailable: retry later" }, "504": { "description": "The request took longer than SENSAPP_HTTP_SERVER_TIMEOUT_SECONDS, usually because the storage backend hangs" } }, "summary": "List unique metrics (measurement types) with aggregated information in DCAT catalog format.", "tags": [ "SensApp" ] } }, "/prometheus/metrics": { "get": { "operationId": "prometheus_metrics", "parameters": [ { "description": "Append the most recent sample for Prometheus-compatible series", "in": "query", "name": "include_latest_samples", "required": false, "schema": { "type": "boolean" } }, { "description": "Optional metric name filter used when include_latest_samples=true", "in": "query", "name": "metric", "required": false, "schema": { "type": "string" } }, { "description": "Optional PromQL-style label selector used when include_latest_samples=true", "in": "query", "name": "selector", "required": false, "schema": { "type": "string" } } ], "responses": { "200": { "content": { "text/plain": { "schema": { "type": "string" } } }, "description": "Prometheus-compatible metrics" } }, "tags": [ "Observability" ] } }, "/publish": { "post": { "description": "Accepts sensor data in one of the following formats:\n- **SenML JSON** (RFC 8428): `Content-Type: application/json`\n- **CSV**: `Content-Type: text/csv` or `application/csv`\n- **Apache Arrow IPC**: `Content-Type: application/vnd.apache.arrow.stream`\n\nIf no Content-Type header is provided, defaults to CSV format.", "operationId": "publish_sensors_data", "requestBody": { "content": { "text/plain": { "schema": { "type": "string" } } }, "description": "Sensor data in SenML JSON, CSV, or Apache Arrow format", "required": true }, "responses": { "200": { "content": { "text/plain": { "schema": { "type": "string" } } }, "description": "Data ingested successfully" }, "400": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/AppError" } } }, "description": "Bad Request - invalid data format" }, "500": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/AppError" } } }, "description": "Internal Server Error" }, "503": { "description": "The storage backend is unavailable, or SensApp is busy writing and sheds the write. A `Retry-After` header (seconds, randomised) means the request was not processed at all and can be sent again after that delay", "headers": { "Retry-After": { "description": "Seconds to wait before sending the write again", "schema": { "format": "int32", "minimum": 0, "type": "integer" } } } }, "504": { "description": "The request took longer than SENSAPP_HTTP_SERVER_TIMEOUT_SECONDS, usually because the storage backend hangs" } }, "summary": "SensApp native data ingestion API supporting multiple formats.", "tags": [ "SensApp" ] } }, "/series": { "get": { "operationId": "list_series", "parameters": [ { "description": "Filter series by metric name", "in": "query", "name": "metric", "required": false, "schema": { "type": "string" } }, { "description": "PromQL-style label selector (e.g., '{env=\"prod\",region=~\"us.*\"}')", "in": "query", "name": "selector", "required": false, "schema": { "type": "string" } } ], "responses": { "200": { "content": { "application/json": { "schema": {} } }, "description": "Time series catalog in DCAT format" }, "503": { "description": "The storage backend is unavailable: retry later" }, "504": { "description": "The request took longer than SENSAPP_HTTP_SERVER_TIMEOUT_SECONDS, usually because the storage backend hangs" } }, "summary": "List all series (time series) in DCAT catalog format.", "tags": [ "SensApp" ] } }, "/series/{series_uuid}": { "delete": { "description": "Publishing the same sensor again recreates it with the same UUID.\nRequires the `delete` scope when authentication is enabled.", "operationId": "delete_series", "parameters": [ { "description": "UUID of the series to delete", "in": "path", "name": "series_uuid", "required": true, "schema": { "type": "string" } } ], "responses": { "204": { "description": "Series deleted" }, "400": { "description": "Invalid UUID" }, "403": { "description": "The token does not have the delete scope" }, "404": { "description": "Series not found" }, "501": { "description": "The storage backend does not support deletion" }, "503": { "description": "The storage backend is unavailable: retry later" }, "504": { "description": "The request took longer than SENSAPP_HTTP_SERVER_TIMEOUT_SECONDS, usually because the storage backend hangs" } }, "summary": "Delete a series: all its samples, its labels and the sensor itself.", "tags": [ "Admin" ] }, "get": { "operationId": "get_series_data", "parameters": [ { "description": "UUID of the series", "in": "path", "name": "series_uuid", "required": true, "schema": { "type": "string" } }, { "description": "Output format: senml, csv, or jsonl (default: senml)", "in": "query", "name": "format", "required": false, "schema": { "type": "string" } }, { "description": "Start datetime in ISO 8601 format (e.g., '2024-01-15T10:30:00Z')", "in": "query", "name": "start", "required": false, "schema": { "type": "string" } }, { "description": "End datetime in ISO 8601 format (e.g., '2024-01-15T11:00:00Z')", "in": "query", "name": "end", "required": false, "schema": { "type": "string" } }, { "description": "Maximum number of samples (at most 100,000). Returns the oldest N samples of the window, in time order, and silently drops the rest. With simplify, it bounds the rows read before simplification", "in": "query", "name": "limit", "required": false, "schema": { "minimum": 0, "type": "integer" } }, { "description": "Bucket width using Prometheus duration syntax (e.g., '1h', '5m', '1d')", "in": "query", "name": "step", "required": false, "schema": { "type": "string" } }, { "description": "Aggregation function: avg, min, max, sum, count, first, last", "in": "query", "name": "aggregation", "required": false, "schema": { "type": "string" } }, { "description": "Explicitly enable simplify-based point reduction. Applied to the rows read for the window (after bucketing when step is set); more than 100,000 rows to simplify is rejected with HTTP 400 instead of being truncated, so use step/aggregation or a narrower start/end", "in": "query", "name": "simplify", "required": false, "schema": { "type": "boolean" } }, { "description": "Dimensionless simplify tolerance on normalized time/value coordinates", "in": "query", "name": "simplify_tolerance", "required": false, "schema": { "format": "double", "type": "number" } }, { "description": "Use Douglas-Peucker only when simplifying", "in": "query", "name": "simplify_high_quality", "required": false, "schema": { "type": "boolean" } } ], "responses": { "200": { "content": { "application/json": { "schema": {} } }, "description": "Series data in requested format" }, "400": { "description": "Invalid format" }, "404": { "description": "Series not found" }, "503": { "description": "The storage backend is unavailable: retry later" }, "504": { "description": "The request took longer than SENSAPP_HTTP_SERVER_TIMEOUT_SECONDS, usually because the storage backend hangs" } }, "summary": "Get series data in various formats based on query parameter.", "tags": [ "SensApp" ] } }, "/series/{series_uuid}/availability": { "get": { "operationId": "get_series_availability", "parameters": [ { "description": "UUID of the series", "in": "path", "name": "series_uuid", "required": true, "schema": { "type": "string" } }, { "description": "Inclusive start datetime in ISO 8601 format", "in": "query", "name": "start", "required": true, "schema": { "type": "string" } }, { "description": "Inclusive end datetime in ISO 8601 format", "in": "query", "name": "end", "required": true, "schema": { "type": "string" } }, { "description": "Optional bucket width using Prometheus duration syntax for coverage calculation", "in": "query", "name": "step", "required": false, "schema": { "type": "string" } } ], "responses": { "200": { "content": { "application/json": { "schema": {} } }, "description": "Availability information for the requested series and window" }, "400": { "description": "Invalid query parameters" }, "404": { "description": "Series not found" }, "503": { "description": "The storage backend is unavailable: retry later" }, "504": { "description": "The request took longer than SENSAPP_HTTP_SERVER_TIMEOUT_SECONDS, usually because the storage backend hangs" } }, "summary": "Get presence and optional bucket coverage for a series over a time window.", "tags": [ "SensApp" ] } }, "/series/{series_uuid}/last": { "get": { "operationId": "get_series_last_sample", "parameters": [ { "description": "UUID of the series", "in": "path", "name": "series_uuid", "required": true, "schema": { "type": "string" } }, { "description": "Optional inclusive start datetime in ISO 8601 format", "in": "query", "name": "start", "required": false, "schema": { "type": "string" } }, { "description": "Optional inclusive end datetime in ISO 8601 format", "in": "query", "name": "end", "required": false, "schema": { "type": "string" } } ], "responses": { "200": { "content": { "application/json": { "schema": {} } }, "description": "Latest sample for the requested series" }, "400": { "description": "Invalid query parameters" }, "404": { "description": "Series not found or no sample matched the requested window" }, "503": { "description": "The storage backend is unavailable: retry later" }, "504": { "description": "The request took longer than SENSAPP_HTTP_SERVER_TIMEOUT_SECONDS, usually because the storage backend hangs" } }, "summary": "Get the most recent sample for a series, optionally within a bounded time window.", "tags": [ "SensApp" ] } }, "/series/{series_uuid}/samples": { "delete": { "description": "Both bounds are required, so that a request cannot wipe a whole series by\naccident (use `DELETE /series/{series_uuid}` for that). `start` equal to `end`\ndeletes the samples at one exact timestamp. The sensor and its labels are kept.\nRequires the `delete` scope when authentication is enabled.", "operationId": "delete_series_samples", "parameters": [ { "description": "UUID of the series", "in": "path", "name": "series_uuid", "required": true, "schema": { "type": "string" } }, { "description": "Inclusive start datetime in ISO 8601 format", "in": "query", "name": "start", "required": true, "schema": { "type": "string" } }, { "description": "Inclusive end datetime in ISO 8601 format", "in": "query", "name": "end", "required": true, "schema": { "type": "string" } } ], "responses": { "200": { "content": { "application/json": { "schema": {} } }, "description": "Number of deleted samples" }, "400": { "description": "Invalid UUID, or missing or invalid time bounds" }, "403": { "description": "The token does not have the delete scope" }, "404": { "description": "Series not found" }, "501": { "description": "The storage backend does not support deletion" }, "503": { "description": "The storage backend is unavailable: retry later" }, "504": { "description": "The request took longer than SENSAPP_HTTP_SERVER_TIMEOUT_SECONDS, usually because the storage backend hangs" } }, "summary": "Delete the samples of a series between `start` and `end`, both inclusive.", "tags": [ "Admin" ] } } }, "tags": [ { "description": "SensApp API", "name": "SensApp" }, { "description": "InfluxDB Write API", "name": "InfluxDB" }, { "description": "Prometheus-compatible service metrics", "name": "Observability" }, { "description": "Prometheus Remote Write and Read API", "name": "Prometheus" }, { "description": "Administrative operations", "name": "Admin" }, { "description": "Health check endpoints", "name": "Health" } ] }