{
  "openapi": "3.1.0",
  "info": {
    "title": "OBEREK Dashboard public data API",
    "version": "1.0.0",
    "summary": "Read-only access to the data shown on the OBEREK Dashboard for l'Albufera de València.",
    "description": "Time series and heatmaps are files in an S3-compatible bucket, read with a plain GET. Each time series is one `latest.json` file holding a daily value for the last 30 days, rewritten once a day; there are no older series files. For a longer or finer-grained history, use the bulk exports: every reading and every KEI calculation as produced, not averaged. Heatmaps keep one PNG per date as well as `latest.png`. Use the listing operation to see which files exist. A key that does not exist returns 404, which means there is no data for that combination, not a server fault. Responses carry an `ETag`, so a request with `If-None-Match` returns 304 when nothing changed. The gateway currently labels JSON files `application/octet-stream`; read the body as JSON regardless. Identifiers in response bodies are CURIEs; the prefixes are listed on the dashboard's Help page.\n\nThe data come from several publishers, each with its own licence and credit requirements. Before reusing any of it, check the attribution table on the Help page: /help#data-sources."
  },
  "externalDocs": {
    "description": "Data sources, publishers and licences",
    "url": "/help#data-sources"
  },
  "servers": [
    { "url": "/", "description": "The dashboard's own origin" }
  ],
  "tags": [
    { "name": "Time series", "description": "Daily series for the last 30 days, one file per location and parameter." },
    { "name": "Heatmaps", "description": "Sentinel-2 water-quality rasters." },
    { "name": "Bulk exports", "description": "Every reading and KEI calculation as produced, one file per pipeline run, in CSV, JSON, Turtle or Jelly." },
    { "name": "Listing", "description": "What files exist in the bucket." },
    { "name": "Community poll", "description": "Aggregated community-perception poll results (KEI-3)." }
  ],
  "paths": {
    "/": {
      "servers": [{ "url": "{s3Endpoint}/{bucket}", "description": "Read-only S3 gateway on the dashboard's origin", "variables": { "s3Endpoint": { "default": "/s3" }, "bucket": { "default": "oberek-bucket" } } }],
      "get": {
        "tags": ["Listing"],
        "operationId": "listObjects",
        "summary": "List files in the bucket",
        "description": "Standard S3 ListObjectsV2. Use it to find which series exist at a location, which heatmap dates are archived, or the bulk exports under `raw/` and `keis/`. The same listing is what `aws s3 ls` uses.",
        "parameters": [
          { "name": "list-type", "in": "query", "required": true, "schema": { "type": "string", "const": "2" } },
          { "name": "prefix", "in": "query", "schema": { "type": "string" }, "example": "heatmaps/chlorophyll/global/" },
          { "name": "delimiter", "in": "query", "description": "Use `/` to list one folder level at a time.", "schema": { "type": "string" }, "example": "/" },
          { "name": "max-keys", "in": "query", "schema": { "type": "integer", "maximum": 1000 } },
          { "name": "continuation-token", "in": "query", "description": "`NextContinuationToken` from the previous page.", "schema": { "type": "string" } }
        ],
        "responses": {
          "200": {
            "description": "S3 `ListBucketResult` XML: one `Contents/Key` per file and, with a delimiter, one `CommonPrefixes/Prefix` per folder.",
            "content": { "application/xml": { "schema": { "type": "string" } } }
          }
        }
      }
    },
    "/{location}/raw/{source}/{feature}/latest.json": {
      "servers": [{ "url": "{s3Endpoint}/{bucket}", "description": "Read-only S3 gateway on the dashboard's origin", "variables": { "s3Endpoint": { "default": "/s3" }, "bucket": { "default": "oberek-bucket" } } }],
      "get": {
        "tags": ["Time series"],
        "operationId": "getMeasuredSeries",
        "summary": "Series measured by a station or satellite",
        "description": "IoT sensors exist at north, south and sw only; the AVAMET weather station at south only. For the flow gauging stations use the `csv-water` path, which adds a station segment.",
        "parameters": [
          {
            "name": "location", "in": "path", "required": true,
            "schema": { "type": "string", "enum": ["global", "north", "south", "sw"] },
            "example": "north"
          },
          {
            "name": "source", "in": "path", "required": true,
            "schema": { "type": "string", "enum": ["iot", "avamet", "copernicus"] }
          },
          { "$ref": "#/components/parameters/feature" }
        ],
        "responses": {
          "200": { "$ref": "#/components/responses/Series" },
          "404": { "$ref": "#/components/responses/NoSuchKey" }
        }
      }
    },
    "/{location}/raw/csv-water/{station}/{feature}/latest.json": {
      "servers": [{ "url": "{s3Endpoint}/{bucket}", "description": "Read-only S3 gateway on the dashboard's origin", "variables": { "s3Endpoint": { "default": "/s3" }, "bucket": { "default": "oberek-bucket" } } }],
      "get": {
        "tags": ["Time series"],
        "operationId": "getFlowGaugingSeries",
        "summary": "Series from one flow gauging station",
        "description": "Published at `global` only, one file per station. Features: `FlowVolume`, `FlowRate`, `wmdr_terr-12252` (water level). Station 08A01 publishes empty flow series.",
        "parameters": [
          {
            "name": "location", "in": "path", "required": true,
            "schema": { "type": "string", "const": "global" }
          },
          {
            "name": "station", "in": "path", "required": true,
            "schema": { "type": "string", "enum": ["08A01", "08A02", "08A03", "08A04", "08A05", "08A06"] },
            "example": "08A03"
          },
          {
            "name": "feature", "in": "path", "required": true,
            "schema": { "type": "string", "enum": ["FlowVolume", "FlowRate", "wmdr_terr-12252"] }
          }
        ],
        "responses": {
          "200": { "$ref": "#/components/responses/Series" },
          "404": { "$ref": "#/components/responses/NoSuchKey" }
        }
      }
    },
    "/{location}/keis/{kei}/{feature}/latest.json": {
      "servers": [{ "url": "{s3Endpoint}/{bucket}", "description": "Read-only S3 gateway on the dashboard's origin", "variables": { "s3Endpoint": { "default": "/s3" }, "bucket": { "default": "oberek-bucket" } } }],
      "get": {
        "tags": ["Time series"],
        "operationId": "getKeiSeries",
        "summary": "KEI level, score or component",
        "description": "Levels run 1–5 and scores 0–100; higher is better on both. KEI-3 exists at `global` only.",
        "parameters": [
          { "$ref": "#/components/parameters/location" },
          {
            "name": "kei", "in": "path", "required": true,
            "schema": { "type": "string", "enum": ["kei-1", "kei-2", "kei-3"] }
          },
          {
            "name": "feature", "in": "path", "required": true,
            "description": "For example `kei-1` (level), `kei-1_RAW` (score), `kei-2-Aquatic_RAW`.",
            "schema": { "type": "string" },
            "example": "kei-1_RAW"
          }
        ],
        "responses": {
          "200": { "$ref": "#/components/responses/Series" },
          "404": { "$ref": "#/components/responses/NoSuchKey" }
        }
      }
    },
    "/{location}/keis/{feature}/latest.json": {
      "servers": [{ "url": "{s3Endpoint}/{bucket}", "description": "Read-only S3 gateway on the dashboard's origin", "variables": { "s3Endpoint": { "default": "/s3" }, "bucket": { "default": "oberek-bucket" } } }],
      "get": {
        "tags": ["Time series"],
        "operationId": "getCalculatedIndexSeries",
        "summary": "Other calculated index",
        "description": "Calculated indices that are not part of a KEI, such as the rice blast (Pyricularia) risk index, which is published at `south` only.",
        "parameters": [
          {
            "name": "location", "in": "path", "required": true,
            "schema": { "type": "string", "enum": ["global", "north", "south", "sw"] },
            "example": "south"
          },
          {
            "name": "feature", "in": "path", "required": true,
            "schema": { "type": "string" },
            "example": "pyricularia_risk_index"
          }
        ],
        "responses": {
          "200": { "$ref": "#/components/responses/Series" },
          "404": { "$ref": "#/components/responses/NoSuchKey" }
        }
      }
    },
    "/keis/{kei}/{format}/{date}/{file}": {
      "servers": [{ "url": "{s3Endpoint}/{bucket}", "description": "Read-only S3 gateway on the dashboard's origin", "variables": { "s3Endpoint": { "default": "/s3" }, "bucket": { "default": "oberek-bucket" } } }],
      "get": {
        "tags": ["Bulk exports"],
        "operationId": "getKeiExport",
        "summary": "KEI calculation runs",
        "description": "Every value each calculator produced in one run, for all locations. The `sensor` field names the location (`nbi:KEI-1_Calculator-North`).",
        "parameters": [
          {
            "name": "kei", "in": "path", "required": true,
            "schema": { "type": "string", "enum": ["kei-1", "kei-2", "kei-3", "pyricularia-risk-index"] },
            "example": "kei-2"
          },
          { "name": "format", "in": "path", "required": true, "schema": { "$ref": "#/components/schemas/ExportFormat" }, "example": "csv" },
          { "name": "date", "in": "path", "required": true, "description": "UTC day of the pipeline run.", "schema": { "$ref": "#/components/schemas/ExportDate" }, "example": "2026-06-26" },
          { "name": "file", "in": "path", "required": true, "description": "`data_HH_MM_SS.<ext>`, the run time; the extension matches `format`. List the date folder to find the names.", "schema": { "$ref": "#/components/schemas/ExportFile" }, "example": "data_17_19_26.csv" }
        ],
        "responses": {
          "200": { "$ref": "#/components/responses/Export" },
          "404": { "$ref": "#/components/responses/NoSuchKey" }
        }
      }
    },
    "/raw/{source}/{format}/{date}/{file}": {
      "servers": [{ "url": "{s3Endpoint}/{bucket}", "description": "Read-only S3 gateway on the dashboard's origin", "variables": { "s3Endpoint": { "default": "/s3" }, "bucket": { "default": "oberek-bucket" } } }],
      "get": {
        "tags": ["Bulk exports"],
        "operationId": "getSourceExport",
        "summary": "Raw data, single-folder sources",
        "description": "Flow gauging stations (`csv-water`, all six in each file) and Copernicus indices (`copernicus`). Copernicus also has `raw/copernicus/tiff/` with the Sentinel band rasters.",
        "parameters": [
          {
            "name": "source", "in": "path", "required": true,
            "schema": { "type": "string", "enum": ["csv-water", "copernicus"] },
            "example": "csv-water"
          },
          { "name": "format", "in": "path", "required": true, "schema": { "$ref": "#/components/schemas/ExportFormat" }, "example": "json" },
          { "name": "date", "in": "path", "required": true, "description": "UTC day of the pipeline run.", "schema": { "$ref": "#/components/schemas/ExportDate" }, "example": "2026-10-27" },
          { "name": "file", "in": "path", "required": true, "description": "`data_HH_MM_SS.<ext>`, the run time; the extension matches `format`. List the date folder to find the names.", "schema": { "$ref": "#/components/schemas/ExportFile" }, "example": "data_18_25_40.json" }
        ],
        "responses": {
          "200": { "$ref": "#/components/responses/Export" },
          "404": { "$ref": "#/components/responses/NoSuchKey" }
        }
      }
    },
    "/raw/{source}/{site}/{format}/{date}/{file}": {
      "servers": [{ "url": "{s3Endpoint}/{bucket}", "description": "Read-only S3 gateway on the dashboard's origin", "variables": { "s3Endpoint": { "default": "/s3" }, "bucket": { "default": "oberek-bucket" } } }],
      "get": {
        "tags": ["Bulk exports"],
        "operationId": "getSiteExport",
        "summary": "Raw data, sources split by site",
        "description": "IoT stations (`iot` with site `north`, `south` or `southwest`; note `southwest`, not `sw`), the AVAMET weather station (`avamet` with site `estell`) and anonymous poll responses (`polls` with `dashboard` or `manual`). IoT writes a file every few minutes.",
        "parameters": [
          {
            "name": "source", "in": "path", "required": true,
            "schema": { "type": "string", "enum": ["iot", "avamet", "polls"] },
            "example": "iot"
          },
          {
            "name": "site", "in": "path", "required": true,
            "schema": { "type": "string", "enum": ["north", "south", "southwest", "estell", "dashboard", "manual"] },
            "example": "south"
          },
          { "name": "format", "in": "path", "required": true, "schema": { "$ref": "#/components/schemas/ExportFormat" }, "example": "json" },
          { "name": "date", "in": "path", "required": true, "description": "UTC day of the pipeline run.", "schema": { "$ref": "#/components/schemas/ExportDate" }, "example": "2026-10-28" },
          { "name": "file", "in": "path", "required": true, "description": "`data_HH_MM_SS.<ext>`, the run time; the extension matches `format`. List the date folder to find the names.", "schema": { "$ref": "#/components/schemas/ExportFile" }, "example": "data_00_00_02.json" }
        ],
        "responses": {
          "200": { "$ref": "#/components/responses/Export" },
          "404": { "$ref": "#/components/responses/NoSuchKey" }
        }
      }
    },
    "/heatmaps/{layer}/{location}/{date}.png": {
      "servers": [{ "url": "{s3Endpoint}/{bucket}", "description": "Read-only S3 gateway on the dashboard's origin", "variables": { "s3Endpoint": { "default": "/s3" }, "bucket": { "default": "oberek-bucket" } } }],
      "get": {
        "tags": ["Heatmaps"],
        "operationId": "getHeatmap",
        "summary": "Sentinel-2 water-quality heatmap",
        "description": "Rendered from the most recent Sentinel-2 scene with under 10% cloud, which can be weeks old. The image covers the open water associated with the location, not the location's own area. Its colour scale is logarithmic, measured from the reference value.",
        "parameters": [
          {
            "name": "layer", "in": "path", "required": true,
            "schema": { "type": "string", "enum": ["chlorophyll", "secchiDiskDepth", "phycocyanin"] }
          },
          { "$ref": "#/components/parameters/location" },
          { "$ref": "#/components/parameters/date" }
        ],
        "responses": {
          "200": {
            "description": "PNG raster.",
            "content": { "image/png": { "schema": { "type": "string", "contentMediaType": "image/png" } } }
          },
          "404": { "$ref": "#/components/responses/NoSuchKey" }
        }
      }
    },
    "/api/polls/stats": {
      "post": {
        "tags": ["Community poll"],
        "operationId": "getPollStats",
        "summary": "Aggregated poll results",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "poll_id": { "type": "string", "const": "nbi:CommunityPoll" }
                },
                "required": ["poll_id"]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Results per question and number of respondents per category.",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/PollStats" } } }
          },
          "400": {
            "description": "Unknown `poll_id`.",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } }
          }
        }
      }
    }
  },
  "components": {
    "parameters": {
      "location": {
        "name": "location", "in": "path", "required": true,
        "schema": { "type": "string", "enum": ["global", "north", "south", "sw"] }
      },
      "feature": {
        "name": "feature", "in": "path", "required": true,
        "description": "Local name of the parameter's CURIE (`s4mari:Salinity` → `Salinity`). A purely numeric local name keeps its prefix with a hyphen (`wmdr_terr:12252` → `wmdr_terr-12252`). The full list is in the Data Export section of the Help page.",
        "schema": { "type": "string" },
        "example": "Salinity"
      },
      "date": {
        "name": "date", "in": "path", "required": true,
        "description": "`latest` for the newest raster, or the `YYYY-MM-DD` date of an archived one (see the listing operation for which dates exist).",
        "schema": {
          "type": "string",
          "anyOf": [{ "const": "latest" }, { "pattern": "^\\d{4}-\\d{2}-\\d{2}$" }]
        },
        "example": "latest"
      }
    },
    "responses": {
      "Export": {
        "description": "One pipeline run. The format follows the `format` segment. File names are run times, so list the date folder first to find them.",
        "content": {
          "application/json": { "schema": { "type": "array", "items": { "$ref": "#/components/schemas/Record" } } },
          "text/csv": { "schema": { "type": "string" }, "example": "timestamp,device,sensor,feature,statistic_type,unit,value\n2026-10-28T00:06:08.000Z,nbi:Estacion_North,nbi:Estacion_North-ec,s4mari:WaterConductivity,,unit:MicroS-PER-CentiM,1606.0" },
          "text/turtle": { "schema": { "type": "string" } },
          "application/x-jelly-rdf": { "schema": { "type": "string", "contentMediaType": "application/x-jelly-rdf" } }
        }
      },
      "Series": {
        "description": "A daily series. An empty `data` array means nothing was measured; in that case `device`, `feature` and `location` are empty strings too.",
        "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Series" } } }
      },
      "NoSuchKey": {
        "description": "No file for this combination: there is no data here. The body depends on the gateway (S3 XML or plain text); rely on the status code."
      }
    },
    "schemas": {
      "ExportFormat": { "type": "string", "enum": ["csv", "json", "ttl", "jelly"] },
      "ExportDate": { "type": "string", "pattern": "^\\d{4}-\\d{2}-\\d{2}$" },
      "ExportFile": { "type": "string", "pattern": "^data_\\d{2}_\\d{2}_\\d{2}\\.(csv|json|ttl|jelly)$" },
      "Series": {
        "type": "object",
        "required": ["data"],
        "properties": {
          "device": { "type": "string", "examples": ["nbi:Estacion_North"] },
          "feature": { "type": "string", "examples": ["s4mari:Salinity"] },
          "location": { "type": "string", "examples": ["nbi:Location-North"] },
          "data": {
            "type": "array",
            "items": {
              "type": "object",
              "required": ["timestamp", "value"],
              "properties": {
                "timestamp": { "type": "string", "format": "date-time" },
                "value": { "type": "number" }
              }
            }
          }
        }
      },
      "Record": {
        "type": "object",
        "description": "One reading or calculated value, as produced. Missing fields are empty strings.",
        "required": ["timestamp", "device", "sensor", "feature", "unit", "value"],
        "properties": {
          "timestamp": { "type": "string", "format": "date-time" },
          "device": { "type": "string", "examples": ["nbi:Estacion_North"] },
          "sensor": { "type": "string", "examples": ["nbi:Estacion_North-ec", "nbi:KEI-1_Calculator-North"] },
          "feature": { "type": "string", "examples": ["s4mari:WaterConductivity"] },
          "statistic_type": { "type": "string", "description": "Summary statistic, empty for a raw value.", "examples": ["", "sst:66851a3"] },
          "unit": { "type": "string", "examples": ["unit:MicroS-PER-CentiM"] },
          "value": { "type": "number" }
        }
      },
      "PollStats": {
        "type": "object",
        "required": ["total_responses", "last_updated", "questions", "demographics_or_categories"],
        "properties": {
          "total_responses": { "type": "integer" },
          "last_updated": { "type": "string", "format": "date-time" },
          "questions": {
            "description": "Keyed by question CURIE (`nbi:CommunityPoll-q1` … `-q4`).",
            "type": "object",
            "additionalProperties": {
              "type": "object",
              "required": ["average_score", "distribution"],
              "properties": {
                "average_score": {
                  "type": "number",
                  "description": "Mean of answers 1–5 (higher is better). \"Don't know\" answers are not counted."
                },
                "distribution": {
                  "description": "Number of answers per score. `0` is \"Don't know / No opinion\".",
                  "type": "object",
                  "propertyNames": { "enum": ["0", "1", "2", "3", "4", "5"] },
                  "additionalProperties": { "type": "integer" }
                }
              }
            }
          },
          "demographics_or_categories": {
            "description": "Number of respondents per category CURIE, for example `nb:LocalResident`.",
            "type": "object",
            "additionalProperties": { "type": "integer" }
          }
        }
      },
      "Error": {
        "type": "object",
        "properties": { "error": { "type": "string" } }
      }
    }
  }
}
