{"openapi":"3.1.0","info":{"title":"DIENAS API","version":"3.4.0","description":"**Distributed IoT Events, Notifications & Automation Service**\n\nAPI documentation for devices, incremental updates, measurements, events and webhooks.\n\n## Authentication limits\n\nMore than 15 failed token authentications from the same IP within 15 minutes trigger a 15-minute IP block. During the block, token-authenticated requests return **HTTP 429**, including requests with a valid token. The `Retry-After` response header gives the remaining wait in seconds. Wait before retrying and check your configured `X-API-Token`. Missing tokens on protected endpoints count as failures. Successful requests do not reset the failed-attempt counter. Clients sharing a public IP share this limit. Public `/health` requests without a token remain available. Password sign-in uses a separate IP counter with the same threshold.\n\n## Quick start\n\nThis documentation includes a public climate sensor that sends a new measurement approximately every five minutes. You can use it to test every read-only endpoint without requesting credentials.\n\n- Demo organization: `Public Demo Organization`\n- Demo device: `78:1C:3C:21:4F:54`\n- Demo API token: `595a321cd5eedc5f6dd461d60a574719e142b5e53bee1a0e`\n\nSend the token in the `X-API-Token` header with every `/api/v1/*` request. Start with `GET /api/v1/devices`, then use the demo MAC address with `/devices/{mac}`, `/devices/{mac}/updates`, `/measurements`, or `/events`.\n\n## Postman\n\nDownload the ready-to-use [Postman Collection](https://dienas.lv/postman_collection.json), or select **Import → Link** in Postman and enter `https://dienas.lv/postman_collection.json`. The collection already contains the public demo token, device MAC address, and example requests.\n\n```bash\ncurl -H \"X-API-Token: 595a321cd5eedc5f6dd461d60a574719e142b5e53bee1a0e\" \\\n  \"https://dienas.lv/api/v1/devices\"\n```\n\nAll timestamps are returned in UTC using ISO 8601. The trailing `Z` means UTC; for example, `2026-09-01T16:56:37Z` is `2026-09-01 19:56:37` in Riga while daylight-saving time is UTC+3. Client applications should parse the value as a date and display it in the viewer's local time zone. Device data is isolated by the organization associated with the supplied token.\n\n\n## Three kinds of history\n\n| History | What it answers | API request |\n|---|---|---|\n| Readings history | What did the device report? Temperature, humidity, open/closed contact position or on/off relay state. | `GET /api/v1/measurements?device=MAC` |\n| Device events | What happened to the device itself? Power-on, reboot, watchdog, crash, wakeup or sleep. | `GET /api/v1/events?device=MAC` |\n| Command history (Relay only) | What action was requested, and what is its execution status? | `GET /api/v1/measurements?device=MAC&type=commands` |\n\nReadings may repeat the same value or state. Opening a door is a contact reading, not a device lifecycle event. An ON command may still be pending or may expire before delivery. Even an applied command does not prove the relay is still ON: a local button may subsequently switch it OFF. Check the device's latest reported state for the last observed value.\n\nClimate and Contact have readings and device events; Relay also has command history. Readings are returned in `data`, device events in `events`, and commands in `commands`. The existing URLs and response formats are unchanged.\n\n## Climate: temperature and humidity\n\nFields in `latest.data`: `temperature_c` (°C), `humidity_percent` (%), and optional `battery_voltage_v` (V). Optional `reboot_reason` is diagnostic information. Missing battery data means not reported, not zero.\n\n`GET /api/v1/devices/78:1C:3C:21:4F:54` — example response:\n\n```json\n{\"organization\":\"Public Demo Organization\",\"mac\":\"78:1C:3C:21:4F:54\",\"device_type\":\"climate\",\"last_seen\":\"2026-09-15T10:00:00Z\",\"latest\":{\"data\":{\"temperature_c\":24.3,\"humidity_percent\":60,\"battery_voltage_v\":2.88},\"quality\":{\"status\":\"valid\",\"issues\":[]}}}\n```\n\nHistory: `GET /api/v1/measurements?device=78:1C:3C:21:4F:54&period=7d&interval=1h`.\nHourly buckets contain `avg`, `min`, `max`, `last`. To receive individual measurements instead, use `interval=raw&page=1&limit=500`. Without an interval, Climate selects raw or aggregation based on the requested duration.\n\n## Contact: doors, windows and gates\n\nFields: `contact_state` is `open` or `closed`; `battery_voltage_v` and diagnostic `reboot_reason` are optional.\n\n`GET /api/v1/devices/B8:06:0D:78:48:4B` — example response:\n\n```json\n{\"organization\":\"Public Demo Organization\",\"mac\":\"B8:06:0D:78:48:4B\",\"device_type\":\"contact\",\"last_seen\":\"2026-09-15T10:00:00Z\",\"latest\":{\"data\":{\"contact_state\":\"closed\",\"battery_voltage_v\":2.85},\"quality\":{\"status\":\"valid\",\"issues\":[]}}}\n```\n\nHistory: `GET /api/v1/measurements?device=B8:06:0D:78:48:4B&period=7d&order=desc&page=1&limit=10`.\nOmitted interval automatically means `raw` for Contact, even for long periods. Add `contact_state=open` to filter open readings. Consecutive readings may repeat the same state; this is reported history, not only transitions.\n\n## Relay: state and control\n\nFields: `latest.data.relay_state` and `reported_state` are `on` or `off` (reported_state may be null when unavailable). Optional `reboot_reason` describes startup. `command` contains the latest command or null; it is not the current physical state.\n\n`GET /api/v1/devices/AA:BB:CC:DD:EE:03` — illustrative response; replace the MAC with your relay and use a private token:\n\n```json\n{\"organization\":\"Example Organization\",\"mac\":\"AA:BB:CC:DD:EE:03\",\"device_type\":\"relay\",\"last_seen\":\"2026-09-15T10:00:00Z\",\"latest\":{\"data\":{\"relay_state\":\"off\"},\"quality\":{\"status\":\"valid\",\"issues\":[]}},\"reported_state\":\"off\",\"command\":null}\n```\n\nState history: `GET /api/v1/measurements?device=AA:BB:CC:DD:EE:03&period=7d&page=1&limit=500`.\nOmitted interval automatically means `raw` for Relay. Command history is separate: add `type=commands` (its records are in `commands`, not `data`).\n\nRelay polling still runs every 10 seconds, but history stores the first report and state changes, plus lifecycle events. Repeated unchanged polls update `last_seen` without adding readings or triggering measurement webhooks. Raw relay history includes `initial_state` (the last stored reading before `from`, or null) and `last_seen`. These context fields are not included in `data`, `count`, or pagination. For a step chart, carry the initial state into the range and extend the last known state only to the earlier of `to` and `last_seen`. This is last-known state, not proof of continuous connectivity. Update IDs remain monotonic but may have gaps after historical duplicate cleanup.\n\nControl: `POST /api/v1/devices/AA:BB:CC:DD:EE:03` with `{\"relay\":\"on\",\"expires_in\":60}`. HTTP 202 means queued, not executed. Use `off` to switch off; expiry is not an automatic OFF timer.\n\nFor all three types, send `X-API-Token`. Examples are illustrative, not live values. Use fixed `from`/`to` dates and pagination to export history. Explicit numeric intervals still aggregate only numeric fields: they omit Contact and Relay text states.\n\n## JavaScript and AJAX\n\nThe API supports cross-origin browser requests with CORS. Send the API token as a request header:\n\n```javascript\nconst response = await fetch(\n  \"https://dienas.lv/api/v1/devices/78:1C:3C:21:4F:54\",\n  { headers: { \"X-API-Token\": \"595a321cd5eedc5f6dd461d60a574719e142b5e53bee1a0e\" } }\n);\nconst device = await response.json();\nconsole.log(device.latest);\n```\n\n## Polling for updates\n\nUse `GET /api/v1/devices/{mac}/updates` when an application needs only records it has not seen yet. The first request without `after_id` returns the newest record. Save its `last_id`; subsequent requests send that value as `after_id`. An empty `updates` array means nothing new has arrived. If `has_more` is true, immediately request the next batch using the returned `last_id`.\n\nIDs are a sequence local to one organization and device. They start at `1` and increase by one for every stored measurement or lifecycle event from that device. A different device has its own independent sequence. Clients should still reuse the exact `last_id` returned by the API rather than calculating a cursor themselves.\n\n## Measurements and events\n\nOrdinary measurements have no `event` property. The current state appears in `/devices/{mac}` under `latest`, and historical records appear in `/measurements`. The optional `event` property is reserved for device lifecycle events; `/events` returns only those events.\n\nEvery current or historical measurement also contains a non-destructive `quality` assessment. `quality.status` is `valid`, `suspicious`, or `invalid`, and `quality.issues` explains each flagged field. The original value in `data` is never removed, corrected, or rejected: for example, a reported `temperature_c` of `125` remains `125` but is marked `suspicious`, helping developers diagnose a wrong sensor driver or scale. Quality rules are conservative heuristics, not calibrated safety limits. Aggregated intervals include `flagged_samples` and issue occurrence counts. Applications may warn users or omit flagged values from business calculations, but should retain the raw response for diagnostics.\n\n`boot` means power-on, `reboot` means a software restart, and `watchdog` or `crash` identifies an abnormal restart. `wakeup` means that a device entered its service or configuration mode, while `sleep` means that it entered deep sleep. Contact position is a measurement: `data.contact_state` is `open` or `closed`, suitable for doors, gates and windows. It is not a lifecycle event. Use `/measurements?device=...&interval=raw&contact_state=open` to find open-state records. These are reported states, not inferred transitions; repeated open readings may appear.\n\nFor compatible battery devices, `data.reboot_reason` contains the low-level numeric startup reason: `0` power-on, `1` software reboot, `2` watchdog, `3` GPIO/button wakeup, and `4` timer wakeup. A sudden physical power loss cannot be reported by an unpowered device; clients should infer that state from `last_seen` when expected telemetry stops.\n\n## Selecting a history range\n\n- Relative hours: `period=12h`\n- Relative days: `period=5d`\n- Relative calendar months: `period=3m`\n- Relative years: `period=1y`\n- Exact UTC range: `from=2026-01-01T00:00:00Z&to=2026-02-01T00:00:00Z`\n\nThe maximum range is five years. The `m` suffix in `period` means calendar months; minute-based aggregation is selected separately with `interval=15m`.\n\n## Webhooks\n\nWebhooks send an HTTPS `POST` request when selected device data matches your rules. The same `X-API-Token` is used to create, list, update, test and delete webhooks. A signing secret is returned only once when the webhook is created. Webhook list requests return up to 100 items without requiring pagination parameters. If there are more than 100, use `page` and `limit`.\n\nDestination URLs can contain measurement placeholders: `{{temperature_c}}`, `{{humidity_percent}}`, `{{battery_voltage_v}}`, `{{device_mac}}`, `{{device_type}}`, `{{event}}`, `{{event_type}}`, and `{{time}}`. Values are URL-encoded automatically immediately before delivery. This makes it possible to send the actual temperature or humidity directly through a Telegram Bot API URL.\n\n## Webhook recipe guide\n\nOpen **Create a webhook → Request body → Examples** to choose a complete request. The same scenarios are available in Postman under **Webhooks → More webhook recipes**. Examples do not create subscriptions automatically; executing POST does. Replace `https://example.com/webhook` with your receiver, and replace illustrative MAC addresses with your actual sensors. Numerical thresholds are illustrative, not hardware safety recommendations.\n\n| Goal | Recipe / behavior |\n| --- | --- |\n| Battery below a configured level | Low battery and recovery |\n| Hot AND humid | Temperature AND humidity; both conditions must match |\n| Humidity outside 40–75% | Create two webhooks: below 40% and above 75% |\n| Gate/door opens or closes | `contact_state eq open` with trigger and recovery |\n| Reboot or crash | Forward reported lifecycle events |\n| Mirror every reading | Forward all organization readings; potentially high volume |\n| Battery voltage changes | Delta examples with independent per-device baselines |\n| Two metrics both change | Delta with match=all |\n| Custom sensor field | Custom pressure metric example |\n\nFor the sequence `24 → 29 → 30 → 25` with `temperature_c > 28`: `on_trigger` notifies at 29; `on_change` notifies at 29 and sends recovery at 25; `every_match` notifies at 29 and 30. Conditions for different metrics use `match=any/all`; multiple rules on the same metric always use AND. For example, `gte:40, lte:75` matches humidity **inside** the range, not outside it. Missing metrics do not match.\n\nThese examples do not imply cooldowns, hysteresis, offline detection or scheduled summaries. Telegram templates for trigger-and-recovery notifications should include `{{event_type}}` so recovery is not mislabeled as an alarm. For custom fields use `{{data.pressure_kpa}}`; for signed changes use `{{delta.temperature_c}}` or `{{delta.battery_voltage_v}}`.\n\n## Notify on a value change\n\nUse `mode=on_delta` with positive `delta` thresholds. This works for any numeric metric, in its normal API units: temperature in °C, humidity in percentage points, battery voltage in volts.\n\nThe first numeric value for each device/metric sets a silent baseline. Changes accumulate relative to that baseline, not the preceding measurement. For a threshold of 1: `24 → 24.4 → 24.8 → 25.1` sends one notification at 25.1; a later 24.0 sends another. A jump of 3 degrees sends one notification, not three.\n\n`match=any` triggers when any metric reaches its threshold; only triggering metrics get new baselines. `match=all` requires all metrics to reach their thresholds in the same received sample. Missing/non-numeric values do not trigger or erase baselines. Baselines and queued notifications are saved together; delivery retries do not move the baseline again. Editing conditions, mode, match, devices or events resets baselines. Pause/resume retains them.\n\nDeliveries have `type=measurement.changed` and a `changes` object, for example `{\"temperature_c\":{\"previous\":24,\"current\":25.1,\"delta\":1.1,\"threshold\":1}}`. `delta` is signed. Telegram URLs can use `{{temperature_c}}`, `{{delta.temperature_c}}`, `{{previous.temperature_c}}`; custom metrics use `{{data.metric_name}}`. A delta/previous placeholder is empty when that metric did not trigger. No history scan or continuous polling is needed: evaluation happens on incoming samples only.\n\n## Verifying webhook signatures\n\nThe `secret` returned when a webhook is created is not used to decrypt its body. Webhook JSON is not encrypted. The secret lets the receiving server verify that the request was created by this API and that its body was not modified.\n\nEvery delivery includes:\n\n```http\nX-Webhook-Id: evt_...\nX-Webhook-Timestamp: 1788372000\nX-Webhook-Signature: sha256=...\n```\n\nThe signature is calculated as:\n\n```text\nsha256=HMAC-SHA256(secret, timestamp + \".\" + raw_request_body)\n```\n\nAlways use the exact raw body bytes before JSON parsing, compare signatures in constant time, and reject timestamps older than five minutes to reduce replay risk. Store the secret when creating the webhook because it is returned only once.\n\nPHP example:\n\n```php\n$secret = 'whsec_...';\n$rawBody = file_get_contents('php://input');\n$timestamp = $_SERVER['HTTP_X_WEBHOOK_TIMESTAMP'] ?? '';\n$received = $_SERVER['HTTP_X_WEBHOOK_SIGNATURE'] ?? '';\n\nif (!ctype_digit($timestamp) || abs(time() - (int) $timestamp) > 300) {\n    http_response_code(401);\n    exit('Expired webhook');\n}\n\n$expected = 'sha256=' . hash_hmac(\n    'sha256',\n    $timestamp . '.' . $rawBody,\n    $secret\n);\n\nif (!hash_equals($expected, $received)) {\n    http_response_code(401);\n    exit('Invalid signature');\n}\n\n$event = json_decode($rawBody, true, flags: JSON_THROW_ON_ERROR);\nhttp_response_code(204);\n```\n\nNode.js/Express example:\n\n```javascript\nimport crypto from \"node:crypto\";\nimport express from \"express\";\n\nconst app = express();\napp.post(\"/webhook\", express.raw({ type: \"application/json\" }), (req, res) => {\n  const secret = \"whsec_...\";\n  const timestamp = req.header(\"X-Webhook-Timestamp\") ?? \"\";\n  const received = req.header(\"X-Webhook-Signature\") ?? \"\";\n  const age = Math.abs(Date.now() / 1000 - Number(timestamp));\n\n  if (!/^\\d+$/.test(timestamp) || age > 300) return res.sendStatus(401);\n\n  const expected = \"sha256=\" + crypto\n    .createHmac(\"sha256\", secret)\n    .update(timestamp + \".\")\n    .update(req.body)\n    .digest(\"hex\");\n  const a = Buffer.from(expected);\n  const b = Buffer.from(received);\n\n  if (a.length !== b.length || !crypto.timingSafeEqual(a, b)) {\n    return res.sendStatus(401);\n  }\n\n  const event = JSON.parse(req.body.toString(\"utf8\"));\n  return res.sendStatus(204);\n});\n```\n\nTelegram Bot API destinations do not validate these custom headers, so signature verification applies primarily to webhooks received by your own sites and services."},"servers":[{"url":"/","description":"Current server"}],"tags":[{"name":"Service","description":"API availability"},{"name":"Devices","description":"Registered devices and their latest state"},{"name":"Data","description":"Measurements and device events"},{"name":"Webhooks","description":"Outgoing notifications and their delivery history"}],"paths":{"/health":{"get":{"tags":["Service"],"summary":"Check API availability","operationId":"health","description":"Checks the API and a live MySQL query without requiring authentication.\n\nOptionally send `X-API-Token` to include `checks.webhook_queue` for that token's organization only. Without a token, queue information is omitted. An invalid supplied token returns `401`; it is not treated as an anonymous request.\n\n`pending` includes both pending and retrying deliveries. `failed` contains terminal failures. Credentials, SQL errors and totals belonging to other organizations are never exposed.","security":[[],{"ApiToken":[]}],"responses":{"200":{"description":"Health information. Queue information is present only with a valid API token.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Health"},"examples":{"public":{"summary":"Public health check without a token","value":{"status":"ok","time":"2026-09-06T20:15:00Z","version":"3.4.0","checks":{"api":{"status":"ok"},"database":{"status":"ok","response_time_ms":4}}}},"authenticated":{"summary":"Organization-scoped queue information with a valid token","value":{"status":"ok","time":"2026-09-06T20:15:00Z","version":"3.4.0","checks":{"api":{"status":"ok"},"database":{"status":"ok","response_time_ms":4},"webhook_queue":{"status":"ok","pending":0,"failed":0}}}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"429":{"$ref":"#/components/responses/TooManyAttempts"},"503":{"description":"The API process responds, but a required database-backed check failed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Health"},"examples":{"public":{"summary":"Database check failed without authentication","value":{"status":"degraded","time":"2026-09-06T20:15:00Z","version":"3.4.0","checks":{"api":{"status":"ok"},"database":{"status":"failed","response_time_ms":1000.25}}}},"authenticated":{"summary":"Database check failed with authentication","value":{"status":"degraded","time":"2026-09-06T20:15:00Z","version":"3.4.0","checks":{"api":{"status":"ok"},"database":{"status":"failed","response_time_ms":1000.25},"webhook_queue":{"status":"failed","pending":null,"failed":null}}}}}}}}}}},"/api/v1/devices":{"get":{"tags":["Devices"],"summary":"List devices","operationId":"listDevices","description":"Returns every device visible to the authenticated organization and the most recent state of each device.\n\nUse this endpoint first when the client does not yet know the device MAC address.\n\n```bash\ncurl -H \"X-API-Token: 595a321cd5eedc5f6dd461d60a574719e142b5e53bee1a0e\" \\\n  \"https://dienas.lv/api/v1/devices\"\n```","security":[{"ApiToken":[]}],"responses":{"200":{"description":"Devices and their latest state.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DeviceList"},"example":{"organization":"Public Demo Organization","devices":[{"mac":"78:1C:3C:21:4F:54","device_type":"climate","last_seen":"2026-09-01T16:51:13Z","latest":{"data":{"temperature_c":24.3,"humidity_percent":66,"battery_voltage_v":2.88},"quality":{"status":"valid","issues":[]}}}]}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"429":{"$ref":"#/components/responses/TooManyAttempts"}}}},"/api/v1/devices/{mac}":{"parameters":[{"name":"mac","in":"path","required":true,"schema":{"type":"string"}}],"post":{"tags":["Devices"],"operationId":"createRelayCommand","summary":"Request relay on or off","security":[{"ApiToken":[]}],"description":"Creates one command for an already registered relay in the token's organization. HTTP 202 means queued, NOT executed. The device polls about every 10 seconds after its previous successful response and confirms execution in a subsequent report. Polling pauses on a transport timeout; delivery is not guaranteed.\n\nOnly one pending/sent command is allowed per device (409 if busy). No toggle operation: repeated delivery of the same ID is ignored by the device, preserving local button changes. The current-command endpoint shows the latest command; use GET /measurements?device=MAC&type=commands for earlier commands. Do not blindly retry POST after an ambiguous network failure: GET the latest command first.\n\nexpires_in defaults to 60 seconds (10–300). Expiry stops future delivery, but cannot recall a response already in flight and does not automatically switch the relay off. Device boot interrupts outstanding commands and the device starts OFF. Local controls remain operational without the API. Use a private organization/token: anyone holding that organization's API token can operate its relays; the public demo token must not be used for real equipment.\n\nStatuses: pending = queued; sent = offered to the device, not confirmed; applied = device acknowledged the command ID; expired = no further delivery; interrupted = device reported a fresh boot. reported_state and last_seen show the last observed state independently of command status. Expiration is evaluated when a poll or command API request occurs, without a background worker.\n\nExamples (replace MAC and token):\n```bash\ncurl -X POST \"$BASE_URL/api/v1/devices/AA:BB:CC:DD:EE:03\" -H \"X-API-Token: $PRIVATE_TOKEN\" -H \"Content-Type: application/json\" -d '{\"relay\":\"on\",\"expires_in\":60}'\ncurl \"$BASE_URL/api/v1/devices/AA:BB:CC:DD:EE:03\" -H \"X-API-Token: $PRIVATE_TOKEN\"\n# Once the previous command is finished:\ncurl -X POST \"$BASE_URL/api/v1/devices/AA:BB:CC:DD:EE:03\" -H \"X-API-Token: $PRIVATE_TOKEN\" -H \"Content-Type: application/json\" -d '{\"relay\":\"off\",\"expires_in\":30}'\ncurl \"$BASE_URL/api/v1/measurements?device=AA:BB:CC:DD:EE:03&period=7d&interval=raw&page=1&limit=500\" -H \"X-API-Token: $PRIVATE_TOKEN\"\n```\nThis is experimental remote control, not a safety interlock. Unencrypted device transport must not be used for unattended hazardous loads.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["relay"],"properties":{"relay":{"type":"string","enum":["on","off"]},"expires_in":{"type":"integer","minimum":10,"maximum":300,"default":60}}},"examples":{"on":{"value":{"relay":"on","expires_in":60}},"off":{"value":{"relay":"off","expires_in":30}}}}}},"responses":{"202":{"description":"Queued, not yet applied","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RelayControl"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"description":"Outstanding command or exhausted counter."},"422":{"description":"Invalid relay value or expiry."},"429":{"description":"IP temporarily blocked; inspect Retry-After."}}},"get":{"tags":["Devices"],"summary":"Get a device","operationId":"getDevice","description":"Returns one device and its latest known state. The MAC address is case-insensitive.\n\nSelect a response example below: Climate, Relay, or Contact. Climate readings contain temperature and humidity; contact readings use `contact_state` (`open` or `closed`). Battery voltage is included only when reported by the device. Examples illustrate the response format; use a device MAC belonging to your token.\n\nFor relays, also returns `reported_state` (`on`/`off`) and `command` (latest command, or null). A command status of applied confirms execution was acknowledged, not that the relay remains ON: local controls may subsequently change it. POST to this same URL to request on/off. Use `/api/v1/measurements?device=MAC&type=commands` for command history.\n\n```bash\ncurl -H \"X-API-Token: 595a321cd5eedc5f6dd461d60a574719e142b5e53bee1a0e\" \\\n  \"https://dienas.lv/api/v1/devices/78:1C:3C:21:4F:54\"\n```","security":[{"ApiToken":[]}],"parameters":[{"name":"mac","in":"path","required":true,"description":"Device MAC address.","schema":{"type":"string","example":"78:1C:3C:21:4F:54"}}],"responses":{"200":{"description":"Device and its latest state.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Device"},"examples":{"climate":{"summary":"Climate — temperature, humidity and battery","value":{"organization":"Public Demo Organization","mac":"78:1C:3C:21:4F:54","device_type":"climate","last_seen":"2026-09-01T16:51:13Z","latest":{"data":{"temperature_c":24.3,"humidity_percent":66,"battery_voltage_v":2.88},"quality":{"status":"valid","issues":[]}}}},"relay":{"summary":"Relay — ON, command acknowledged","value":{"organization":"Public Demo Organization","mac":"AA:BB:CC:DD:EE:03","device_type":"relay","last_seen":"2026-09-13T10:00:10Z","latest":{"data":{"relay_state":"on"},"quality":{"status":"valid","issues":[]}},"reported_state":"on","command":{"id":12,"relay":"on","status":"applied","created_at":"2026-09-13T10:00:00Z","expires_at":"2026-09-13T10:01:00Z"}}},"contact":{"summary":"Contact — door/window closed and battery","value":{"organization":"Public Demo Organization","mac":"B8:06:0D:78:48:4B","device_type":"contact","last_seen":"2026-09-13T10:00:00Z","latest":{"data":{"contact_state":"closed","battery_voltage_v":2.85},"quality":{"status":"valid","issues":[]}}}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"429":{"$ref":"#/components/responses/TooManyAttempts"},"404":{"$ref":"#/components/responses/NotFound"}}}},"/api/v1/devices/{mac}/updates":{"get":{"tags":["Devices"],"summary":"Poll for new device records","operationId":"getDeviceUpdates","description":"Returns incremental records for one device in ascending ID order. Both ordinary measurements and lifecycle events are included.\n\nStart by requesting the current record and saving `last_id`:\n\n```bash\ncurl -H \"X-API-Token: 595a321cd5eedc5f6dd461d60a574719e142b5e53bee1a0e\" \\\n  \"https://dienas.lv/api/v1/devices/78:1C:3C:21:4F:54/updates\"\n```\n\nOn every later poll, send the saved ID:\n\n```bash\ncurl -H \"X-API-Token: 595a321cd5eedc5f6dd461d60a574719e142b5e53bee1a0e\" \\\n  \"https://dienas.lv/api/v1/devices/78:1C:3C:21:4F:54/updates?after_id=1533&limit=100\"\n```\n\nIf `updates` is empty, keep the same `last_id` for the next request. When `has_more` is true, request the next batch immediately using the returned `last_id`; otherwise poll again after the application's chosen delay.\n\n`after_id` is the last processed ID in this device's own sequence. Each organization/device pair starts at `1` and advances by one (`1533`, `1534`, `1535`). Other devices have independent sequences. Always save and reuse the returned `last_id`.\n\nJavaScript polling example:\n\n```javascript\nlet lastId = null;\n\nasync function pollDevice() {\n  const suffix = lastId === null ? \"\" : `?after_id=${lastId}&limit=100`;\n  const response = await fetch(\n    `https://dienas.lv/api/v1/devices/78:1C:3C:21:4F:54/updates${suffix}`,\n    { headers: { \"X-API-Token\": \"595a321cd5eedc5f6dd461d60a574719e142b5e53bee1a0e\" } }\n  );\n  const result = await response.json();\n  result.updates.forEach(update => console.log(update.id, update.data));\n  lastId = result.last_id;\n}\n```","security":[{"ApiToken":[]}],"parameters":[{"name":"mac","in":"path","required":true,"description":"Device MAC address.","schema":{"type":"string","example":"78:1C:3C:21:4F:54"}},{"name":"after_id","in":"query","required":false,"description":"Return records with an ID greater than this cursor. Omit it on the first request to receive only the newest record.","schema":{"type":"integer","minimum":0,"example":1533}},{"name":"limit","in":"query","required":false,"description":"Maximum number of updates returned. The API fetches one extra internally to calculate has_more.","schema":{"type":"integer","minimum":1,"maximum":1000,"default":100,"example":100}}],"responses":{"200":{"description":"Current record or records newer than after_id.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateCollection"},"examples":{"first_poll":{"summary":"First poll returns the current record","value":{"device":"78:1C:3C:21:4F:54","device_type":"climate","count":1,"updates":[{"id":1533,"time":"2026-09-06T18:30:00Z","data":{"temperature_c":24.8,"humidity_percent":61,"battery_voltage_v":2.91},"quality":{"status":"valid","issues":[]}}],"last_id":1533,"has_more":false}},"multiple_updates":{"summary":"Several new records arrived","value":{"device":"78:1C:3C:21:4F:54","device_type":"climate","count":2,"updates":[{"id":1534,"time":"2026-09-06T18:35:00Z","data":{"temperature_c":24.9,"humidity_percent":61,"battery_voltage_v":2.9},"quality":{"status":"valid","issues":[]}},{"id":1535,"time":"2026-09-06T18:40:00Z","data":{"temperature_c":25.1,"humidity_percent":60,"battery_voltage_v":2.9},"quality":{"status":"valid","issues":[]}}],"last_id":1535,"has_more":false}},"no_updates":{"summary":"Nothing new has arrived","value":{"device":"78:1C:3C:21:4F:54","device_type":"climate","count":0,"updates":[],"last_id":1535,"has_more":false}},"more_available":{"summary":"The limit was reached; request the next batch","value":{"device":"78:1C:3C:21:4F:54","device_type":"climate","count":1,"updates":[{"id":1534,"time":"2026-09-06T18:35:00Z","data":{"temperature_c":24.9,"humidity_percent":61,"battery_voltage_v":2.9},"quality":{"status":"valid","issues":[]}}],"last_id":1534,"has_more":true}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"429":{"$ref":"#/components/responses/TooManyAttempts"},"404":{"$ref":"#/components/responses/NotFound"},"422":{"$ref":"#/components/responses/ValidationError"}}}},"/api/v1/measurements":{"get":{"tags":["Data"],"summary":"Get measurements for a time range","operationId":"getMeasurements","description":"Returns historical device data. Use `interval=raw` for every stored record or an aggregation interval for chart-friendly data.\n\n### Relay command history\n\nBy default (`type=readings`), this endpoint returns measured states, including relay_state (`on` or `off`). Relay state history requires `interval=raw` for any time range; numeric aggregation omits textual states. The dashboard selects raw automatically for relays. Raw readings support `order=desc` (newest first) or `order=asc` (default). For recent contact history use `interval=raw&order=desc&limit=10&page=1`. Keep from/to fixed when paging. Use `type=commands` to return relay commands instead, in a `commands` array, newest first. These are requests to switch the relay, not measured states. Each entry contains its latest status, not individual delivery attempts.\n\nCommand history supports `period` (default 24h) or exact `from`/`to`, filtered by command creation time. It always uses raw records: omit interval or set `interval=raw`; aggregation is rejected. Command pagination defaults to page=1, limit=20, maximum 100. Its pagination contains total, has_next and has_previous. Unknown/non-relay devices return 404. History is retained until device/organization deletion. Only the last pre-history command could be imported; older overwritten commands cannot be recovered.\n\n```text\n/api/v1/measurements?device=AA:BB:CC:DD:EE:03&type=commands&period=7d&page=1&limit=20\n/api/v1/measurements?device=AA:BB:CC:DD:EE:03&type=commands&from=2026-09-01T00:00:00Z&to=2026-09-14T00:00:00Z&page=2&limit=20\n```\n\nThe remaining aggregation and sample pagination rules below apply to readings.\n\nBecause every request selects one device, `device_type` is returned once at the top level and is not repeated inside each `data` item.\n\nIf `from` and `to` are omitted, `period` is used. The default period is `24h`. A period can contain hours, days, calendar months, or years, for example `12h`, `5d`, `3m`, or `1y`. When `interval` is omitted, the API selects a suitable interval for the requested range.\n\nRaw results use page-based pagination. `page` defaults to 1. `limit` defaults to 500 and can be set from 1 to 1000. The response includes the total number of records and pages plus `has_previous` and `has_next`. For a stable multi-page export while new telemetry is arriving, prefer an exact `from` and `to` range instead of a moving relative period.\n\n### Door, gate and window sensors (`device_type=contact`)\n\nWhen interval is omitted, Contact automatically selects `raw` (Every reading in the dashboard) to retrieve `data.contact_state`, for any time range, including 7 days, 30 days or an exact `from`/`to` range. Omit the `contact_state` filter to receive both `open` and `closed` records and build a step chart; follow pagination to retrieve all records. These are reported positions, so consecutive records may contain the same state.\n\nIntervals `15m`, `1h`, `6h` and `1d` calculate numeric statistics such as average, minimum and maximum. They do not include the text field `contact_state`: averaging open/closed positions would not preserve the sequence of openings and closings. Numeric fields such as battery voltage can still be aggregated. Omitting `interval` selects `raw` for Contact and Relay at all ranges. Climate continues to select its interval based on the requested duration. The dashboard does this automatically for contact devices. This is a query parameter, not a change to the device type or stored data.\n\n```text\n# Both open and closed positions over 30 days\n/api/v1/measurements?device=B8:06:0D:78:48:4B&period=30d&interval=raw&page=1&limit=500\n\n# Both positions within an exact UTC date range\n/api/v1/measurements?device=B8:06:0D:78:48:4B&from=2026-09-01T00:00:00Z&to=2026-09-08T00:00:00Z&interval=raw&page=1&limit=500\n```\n\n```bash\n# First raw page\ncurl -H \"X-API-Token: 595a321cd5eedc5f6dd461d60a574719e142b5e53bee1a0e\" \\\n  \"https://dienas.lv/api/v1/measurements?device=78:1C:3C:21:4F:54&period=24h&interval=raw&page=1&limit=10\"\n\n# Second raw page\ncurl -H \"X-API-Token: 595a321cd5eedc5f6dd461d60a574719e142b5e53bee1a0e\" \\\n  \"https://dienas.lv/api/v1/measurements?device=78:1C:3C:21:4F:54&period=24h&interval=raw&page=2&limit=10\"\n\n# Last five days, automatically aggregated\ncurl -H \"X-API-Token: 595a321cd5eedc5f6dd461d60a574719e142b5e53bee1a0e\" \\\n  \"https://dienas.lv/api/v1/measurements?device=78:1C:3C:21:4F:54&period=5d\"\n\n# Last three calendar months, grouped by day\ncurl -H \"X-API-Token: 595a321cd5eedc5f6dd461d60a574719e142b5e53bee1a0e\" \\\n  \"https://dienas.lv/api/v1/measurements?device=78:1C:3C:21:4F:54&period=3m&interval=1d\"\n\n# Exact date range\ncurl -H \"X-API-Token: 595a321cd5eedc5f6dd461d60a574719e142b5e53bee1a0e\" \\\n  \"https://dienas.lv/api/v1/measurements?device=78:1C:3C:21:4F:54&from=2026-09-01T00:00:00Z&to=2026-09-02T00:00:00Z&interval=1h\"\n```","security":[{"ApiToken":[]}],"parameters":[{"name":"device","in":"query","required":true,"description":"Device MAC address.","schema":{"type":"string","pattern":"^[0-9A-Fa-f]{2}(?::[0-9A-Fa-f]{2}){5}$","example":"78:1C:3C:21:4F:54"}},{"name":"from","in":"query","required":false,"description":"Exact range start in ISO 8601 format. Must be used together with to.","schema":{"type":"string","format":"date-time","example":"2026-01-01T00:00:00Z"}},{"name":"to","in":"query","required":false,"description":"Inclusive exact range end in ISO 8601 format. Must be used together with from.","schema":{"type":"string","format":"date-time","example":"2026-02-01T00:00:00Z"}},{"name":"period","in":"query","required":false,"description":"Relative range used when from/to are omitted. Suffixes: h=hours, d=days, m=calendar months, y=years.","schema":{"type":"string","pattern":"^[1-9][0-9]{0,3}[hdmy]$","default":"24h","example":"3m"}},{"name":"type","in":"query","required":false,"description":"readings returns measurements; commands returns relay command history (no aggregation, default limit 20, maximum 100).","schema":{"type":"string","enum":["readings","commands"],"default":"readings"}},{"name":"interval","in":"query","required":false,"description":"For commands only raw is supported and selected by default. For readings, defaults to raw for Contact/Relay; Climate selects an interval based on the duration. An explicit interval takes precedence.","schema":{"type":"string","enum":["raw","15m","1h","6h","1d"]}},{"name":"limit","in":"query","required":false,"description":"Maximum records per page for interval=raw.","schema":{"type":"integer","minimum":1,"maximum":1000,"default":500,"example":100}},{"name":"page","in":"query","required":false,"description":"Page number for interval=raw.","schema":{"type":"integer","minimum":1,"maximum":1000000,"default":1,"example":1}},{"name":"contact_state","in":"query","required":false,"description":"Filter reported contact position. Requires interval=raw; if interval is omitted, raw is selected. Pagination totals include only matching rows. Use raw history for contact states; numeric aggregation does not aggregate strings.","schema":{"type":"string","enum":["open","closed"],"example":"open"}}],"responses":{"200":{"description":"Measurements for the selected range.","content":{"application/json":{"schema":{"anyOf":[{"$ref":"#/components/schemas/MeasurementCollection"},{"$ref":"#/components/schemas/CommandHistory"}]},"examples":{"relay_commands":{"summary":"Relay commands (type=commands)","value":{"device":"AA:BB:CC:DD:EE:03","type":"commands","interval":"raw","from":"2026-09-01T00:00:00Z","to":"2026-09-14T00:00:00Z","commands":[{"id":1,"relay":"on","status":"applied","created_at":"2026-09-12T18:00:00Z","expires_at":"2026-09-12T18:01:00Z","updated_at":"2026-09-12T18:00:20Z"}],"pagination":{"page":1,"limit":20,"total":1,"has_next":false,"has_previous":false}}},"contact_open":{"summary":"Contact history filtered by contact_state=open","value":{"device":"AA:BB:CC:DD:EE:02","device_type":"contact","from":"2026-09-01T00:00:00Z","to":"2026-09-02T00:00:00Z","interval":"raw","count":2,"data":[{"data":{"contact_state":"open","battery_voltage_v":2.62},"time":"2026-09-01T08:15:00Z"},{"data":{"contact_state":"open","battery_voltage_v":2.61},"time":"2026-09-01T10:28:00Z"}],"pagination":{"page":1,"limit":500,"total_records":2,"total_pages":1,"has_previous":false,"has_next":false}}},"climate_raw":{"summary":"Raw climate measurements","value":{"device":"78:1C:3C:21:4F:54","device_type":"climate","from":"2026-09-01T09:00:00Z","to":"2026-09-01T11:00:00Z","interval":"raw","count":2,"data":[{"time":"2026-09-01T09:00:00Z","data":{"temperature_c":23.8,"humidity_percent":61,"battery_voltage_v":2.54},"quality":{"status":"valid","issues":[]}},{"time":"2026-09-01T10:00:00Z","data":{"temperature_c":24.3,"humidity_percent":66,"battery_voltage_v":2.88},"quality":{"status":"valid","issues":[]}}],"pagination":{"page":1,"limit":2,"total_records":20,"total_pages":10,"has_previous":false,"has_next":true}}},"climate_aggregated":{"summary":"Hourly climate aggregation","value":{"device":"78:1C:3C:21:4F:54","device_type":"climate","from":"2026-09-01T00:00:00Z","to":"2026-09-02T00:00:00Z","interval":"1h","count":1,"data":[{"time":"2026-09-01T10:00:00Z","samples":12,"data":{"temperature_c":{"avg":24.15,"min":23.8,"max":24.5,"last":24.2},"humidity_percent":{"avg":60.4,"min":59,"max":62,"last":60}}}]}}}}}},"429":{"$ref":"#/components/responses/TooManyAttempts"},"401":{"$ref":"#/components/responses/Unauthorized"},"422":{"$ref":"#/components/responses/ValidationError"}}}},"/api/v1/events":{"get":{"tags":["Data"],"summary":"Get device events","operationId":"getEvents","description":"Returns device lifecycle or state-change events in reverse chronological order.\n\nOrdinary measurements and contact states are excluded. Use `type=boot`, `type=reboot`, `type=watchdog`, or `type=crash` to inspect startup history. For contact history, use `/measurements` with `interval=raw` and optional `contact_state=open` or `contact_state=closed`. Use `type=wakeup` or `type=sleep` to audit service-mode wakeups and planned deep-sleep transitions.\n\n```bash\ncurl -H \"X-API-Token: 595a321cd5eedc5f6dd461d60a574719e142b5e53bee1a0e\" \\\n  \"https://dienas.lv/api/v1/events?device=78:1C:3C:21:4F:54&type=wakeup&limit=100\"\n```","security":[{"ApiToken":[]}],"parameters":[{"name":"device","in":"query","required":true,"description":"Device MAC address.","schema":{"type":"string","pattern":"^[0-9A-Fa-f]{2}(?::[0-9A-Fa-f]{2}){5}$","example":"78:1C:3C:21:4F:54"}},{"name":"type","in":"query","required":false,"description":"Event filter.","schema":{"type":"string","enum":["boot","reboot","watchdog","crash","wakeup","sleep"],"example":"reboot"}},{"name":"limit","in":"query","required":false,"description":"Maximum number of records.","schema":{"type":"integer","minimum":1,"maximum":500,"default":100}}],"responses":{"200":{"description":"Device events.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/EventCollection"},"examples":{"lifecycle":{"summary":"Power-on, reboot and sleep","value":{"device":"78:1C:3C:21:4F:54","device_type":"climate","events":[{"event":"sleep","data":{"temperature_c":24.3,"humidity_percent":66,"battery_voltage_v":2.88,"reboot_reason":1},"time":"2026-09-01T10:12:10Z"},{"event":"reboot","data":{"temperature_c":24.3,"humidity_percent":66,"battery_voltage_v":2.88,"reboot_reason":1},"time":"2026-09-01T10:12:00Z"},{"event":"boot","data":{"temperature_c":24.3,"humidity_percent":66,"battery_voltage_v":2.88,"reboot_reason":0},"time":"2026-09-01T08:00:00Z"}]}},"service_mode":{"summary":"Device wakeup and sleep","value":{"device":"78:1C:3C:21:4F:54","device_type":"climate","events":[{"event":"sleep","data":{"temperature_c":24.3,"humidity_percent":66,"battery_voltage_v":2.88},"time":"2026-09-01T10:12:00Z"},{"event":"wakeup","data":{"temperature_c":24.3,"humidity_percent":66,"battery_voltage_v":2.88},"time":"2026-09-01T10:05:00Z"}]}}}}}},"429":{"$ref":"#/components/responses/TooManyAttempts"},"401":{"$ref":"#/components/responses/Unauthorized"},"422":{"$ref":"#/components/responses/ValidationError"}}}},"/api/v1/webhooks":{"get":{"tags":["Webhooks"],"summary":"List webhooks","operationId":"listWebhooks","description":"Without `page` and `limit`, returns up to 100 webhooks. Use `device` to return webhooks that explicitly contain that MAC address plus organization-wide webhooks whose `devices` array is empty. The `pagination` object is omitted when the complete result fits in one default response. It is included when more than 100 webhooks exist or pagination parameters are explicitly supplied.","security":[{"ApiToken":[]}],"parameters":[{"name":"enabled","in":"query","required":false,"description":"Filter enabled or disabled webhooks.","schema":{"type":"boolean"}},{"name":"device","in":"query","required":false,"description":"Filter webhooks applicable to this device. Organization-wide webhooks with an empty devices list are included.","schema":{"type":"string","pattern":"^[0-9A-Fa-f]{2}(?::[0-9A-Fa-f]{2}){5}$","example":"78:1C:3C:21:4F:54"}},{"name":"page","in":"query","required":false,"description":"Optional page number.","schema":{"type":"integer","minimum":1,"maximum":1000000,"example":1}},{"name":"limit","in":"query","required":false,"description":"Optional page size.","schema":{"type":"integer","minimum":1,"maximum":100,"example":20}}],"responses":{"200":{"description":"Webhooks visible to the organization.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/WebhookList"},"examples":{"default":{"summary":"Complete list without pagination","value":{"count":1,"data":[{"id":"wh_4a219a3dd97245c6a46e901b","name":"High climate values","url":"https://example.com/webhook","devices":["78:1C:3C:21:4F:54"],"events":[],"match":"any","mode":"on_change","conditions":{"temperature_c":{"gt":28},"humidity_percent":{"gt":75}},"enabled":true,"last_delivery":null,"created_at":"2026-09-01T17:00:00Z","updated_at":"2026-09-01T17:00:00Z"}]}},"paged":{"summary":"Explicit page and limit","value":{"count":1,"data":[{"id":"wh_4a219a3dd97245c6a46e901b","name":"High climate values","url":"https://example.com/webhook","devices":["78:1C:3C:21:4F:54"],"events":[],"match":"any","mode":"on_change","conditions":{"temperature_c":{"gt":28},"humidity_percent":{"gt":75}},"enabled":true,"last_delivery":null,"created_at":"2026-09-01T17:00:00Z","updated_at":"2026-09-01T17:00:00Z"}],"pagination":{"page":1,"limit":20,"total_records":101,"total_pages":6,"has_previous":false,"has_next":true}}}}}}},"429":{"$ref":"#/components/responses/TooManyAttempts"},"401":{"$ref":"#/components/responses/Unauthorized"},"422":{"$ref":"#/components/responses/ValidationError"}}},"post":{"tags":["Webhooks"],"summary":"Create a webhook","operationId":"createWebhook","description":"Creates an outgoing webhook. An empty `devices` list means every device; omitted `events` or `events: []` selects ordinary measurements only (including contact states). A non-empty `events` array selects only the listed lifecycle events, instead of ordinary measurements. `match=any` fires when at least one metric condition matches, while `match=all` requires all metrics. `mode=on_change` sends both `condition.triggered` and `condition.recovered`; `mode=on_trigger` sends only the transition into the matched state; `mode=every_match` sends every matching sample. `mode=on_delta` sends `measurement.changed` when a numeric value moves up or down by its positive `delta` threshold from the stored baseline. The first sample initializes the baseline without notification. Use only delta rules with this mode.\n\nThe generated `secret` is shown only in this response. Store it securely and use it to verify `X-Webhook-Signature`.\n\nFor Telegram, put Bot API parameters and message placeholders directly in the URL. For example, `text=Temperature:%20{{temperature_c}}%20C`. Keep placeholder braces literal, as `{{temperature_c}}`; spaces and other URL characters remain encoded. The API normalizes encoded placeholder braces when saving a URL.","security":[{"ApiToken":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/WebhookInput"},"examples":{"low_battery":{"summary":"Low battery and recovery","description":"Illustrative threshold only: 2.2 V is not a universal battery limit. Sends condition.triggered below 2.2 V and condition.recovered when voltage returns to 2.2 V or above. Choose a threshold appropriate to your hardware.","value":{"url":"https://example.com/webhook","devices":["78:1C:3C:21:4F:54"],"events":[],"match":"any","name":"Low battery and recovery","mode":"on_change","conditions":{"battery_voltage_v":{"lt":2.2}}}},"hot_and_humid":{"summary":"Temperature AND humidity","description":"Both temperature ≥28 °C and humidity ≥75% must match in the same sample. One trigger when both become true; one recovery when either stops matching.","value":{"url":"https://example.com/webhook","devices":["78:1C:3C:21:4F:54"],"events":[],"match":"all","name":"Temperature AND humidity","mode":"on_change","conditions":{"temperature_c":{"gte":28},"humidity_percent":{"gte":75}}}},"high_humidity":{"summary":"Humidity above 75%","description":"Trigger above 75%, recovery at 75% or below. For an outside-range alert, create this and the separate low-humidity webhook: two rules on the same metric are combined with AND, not OR.","value":{"url":"https://example.com/webhook","devices":["78:1C:3C:21:4F:54"],"events":[],"match":"any","name":"Humidity above 75%","mode":"on_change","conditions":{"humidity_percent":{"gt":75}}}},"low_humidity":{"summary":"Humidity below 40%","description":"Trigger below 40%, recovery at 40% or above. Use alongside the high-humidity example to monitor values outside 40–75%.","value":{"url":"https://example.com/webhook","devices":["78:1C:3C:21:4F:54"],"events":[],"match":"any","name":"Humidity below 40%","mode":"on_change","conditions":{"humidity_percent":{"lt":40}}}},"relay_changes":{"summary":"Relay ON and OFF","description":"Trigger on ON and recover on OFF. Repeated states do not notify. Use {{data.relay_state}} in URL templates. Only eq on/off is supported for relay_state.","value":{"name":"Relay ON and OFF","url":"https://example.com/webhook","devices":["AA:BB:CC:DD:EE:03"],"mode":"on_change","conditions":{"relay_state":{"eq":"on"}}}},"door_events":{"summary":"Door or gate opens and closes","description":"Replace the example MAC with your contact sensor. Trigger when data.contact_state becomes open; send recovery when it becomes closed. Repeated unchanged states do not notify. Use on_trigger for openings only.","value":{"url":"https://example.com/webhook","devices":["AA:BB:CC:DD:EE:02"],"events":[],"match":"any","name":"Door or gate opens and closes","mode":"on_change","conditions":{"contact_state":{"eq":"open"}}}},"restart_events":{"summary":"Device restarts and crashes","description":"Forward every reported reboot, watchdog or crash event. This cannot detect sudden power loss or silence; it only reacts to received events.","value":{"url":"https://example.com/webhook","devices":["78:1C:3C:21:4F:54"],"events":["reboot","watchdog","crash"],"match":"any","name":"Device restarts and crashes","mode":"every_match","conditions":{}}},"all_readings":{"summary":"Forward all organization readings","description":"An empty device list includes all devices belonging to your token. Sends every received reading, without deduplication or rate limiting. Consider destination capacity before enabling.","value":{"url":"https://example.com/webhook","devices":[],"events":[],"match":"any","name":"Forward all organization readings","mode":"every_match","conditions":{}}},"battery_delta":{"summary":"Battery changes by ±0.1 V","description":"First voltage sets a silent baseline; later rises or falls of at least 0.1 V trigger. Gradual changes accumulate. Sensor noise can also trigger this rule.","value":{"url":"https://example.com/webhook","devices":["78:1C:3C:21:4F:54"],"events":[],"match":"any","name":"Battery changes by ±0.1 V","mode":"on_delta","conditions":{"battery_voltage_v":{"delta":0.1}}}},"delta_all":{"summary":"Both temperature AND humidity change","description":"First values initialize silently. Notify only when temperature has moved at least 1 °C AND humidity at least 5 percentage points from their baselines in the same sample. Both baselines then update.","value":{"url":"https://example.com/webhook","devices":["78:1C:3C:21:4F:54"],"events":[],"match":"all","name":"Both temperature AND humidity change","mode":"on_delta","conditions":{"temperature_c":{"delta":1},"humidity_percent":{"delta":5}}}},"custom_metric":{"summary":"Custom numeric metric: pressure","description":"Replace the MAC with your pressure sensor. Any numeric metric is supported; pressure_kpa must actually exist in incoming data. This illustrative rule triggers above 120 kPa and does not send recovery messages.","value":{"url":"https://example.com/webhook","devices":["AA:BB:CC:DD:EE:05"],"events":[],"match":"any","name":"Custom numeric metric: pressure","mode":"on_trigger","conditions":{"pressure_kpa":{"gt":120}}}},"delta_telegram":{"summary":"Telegram on a temperature change of ±1 °C","value":{"name":"Temperature change","url":"https://api.telegram.org/botYOUR_BOT_TOKEN/sendMessage?chat_id=YOUR_CHAT_ID&text=Temperature:%20{{temperature_c}}%20C%2C%20change:%20{{delta.temperature_c}}%20C","devices":["78:1C:3C:21:4F:54"],"events":[],"mode":"on_delta","match":"any","conditions":{"temperature_c":{"delta":1}}}},"delta_metrics":{"summary":"Any change: ±1 °C, ±5 humidity points or ±0.1 V","value":{"name":"Metric changes","url":"https://example.com/webhook","devices":["78:1C:3C:21:4F:54"],"events":[],"mode":"on_delta","match":"any","conditions":{"temperature_c":{"delta":1},"humidity_percent":{"delta":5},"battery_voltage_v":{"delta":0.1}}}},"standard":{"summary":"Standard JSON webhook","value":{"name":"High climate values","url":"https://example.com/webhook","devices":["78:1C:3C:21:4F:54"],"events":[],"match":"any","mode":"on_change","conditions":{"temperature_c":{"gt":28},"humidity_percent":{"gt":75}}}},"telegram":{"summary":"Telegram message containing actual values","value":{"name":"Telegram high temperature","url":"https://api.telegram.org/botYOUR_BOT_TOKEN/sendMessage?chat_id=YOUR_CHAT_ID&text=Temperature:%20{{temperature_c}}%20C%2C%20humidity:%20{{humidity_percent}}%25","devices":["78:1C:3C:21:4F:54"],"events":[],"match":"all","mode":"on_trigger","conditions":{"temperature_c":{"gt":28}}}}}}}},"responses":{"201":{"description":"Webhook created. Save the one-time signing secret.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Webhook"},"example":{"id":"wh_4a219a3dd97245c6a46e901b","name":"High climate values","url":"https://example.com/webhook","devices":["78:1C:3C:21:4F:54"],"events":[],"match":"any","mode":"on_change","conditions":{"temperature_c":{"gt":28},"humidity_percent":{"gt":75}},"enabled":true,"last_delivery":null,"created_at":"2026-09-01T17:00:00Z","updated_at":"2026-09-01T17:00:00Z","secret":"whsec_0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"}}}},"429":{"$ref":"#/components/responses/TooManyAttempts"},"401":{"$ref":"#/components/responses/Unauthorized"},"422":{"$ref":"#/components/responses/ValidationError"}}}},"/api/v1/webhooks/{id}":{"parameters":[{"name":"id","in":"path","required":true,"description":"Webhook ID.","schema":{"type":"string","example":"wh_4a219a3dd97245c6a46e901b"}}],"get":{"tags":["Webhooks"],"summary":"Get a webhook","operationId":"getWebhook","security":[{"ApiToken":[]}],"responses":{"200":{"description":"Webhook configuration.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Webhook"},"example":{"id":"wh_4a219a3dd97245c6a46e901b","name":"High climate values","url":"https://example.com/webhook","devices":["78:1C:3C:21:4F:54"],"events":[],"match":"any","mode":"on_change","conditions":{"temperature_c":{"gt":28},"humidity_percent":{"gt":75}},"enabled":true,"last_delivery":null,"created_at":"2026-09-01T17:00:00Z","updated_at":"2026-09-01T17:00:00Z"}}}},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/TooManyAttempts"},"401":{"$ref":"#/components/responses/Unauthorized"},"422":{"$ref":"#/components/responses/ValidationError"}}},"post":{"tags":["Webhooks"],"summary":"Update a webhook","operationId":"updateWebhook","description":"Updates only the fields included in the JSON body; omitted fields keep their current values. The same endpoint edits configuration and controls the active state.\n\n- Pause: `{\"enabled\": false}`\n- Resume: `{\"enabled\": true}`\n- Change threshold: `{\"conditions\": {\"temperature_c\": {\"gt\": 30}}}`\n- Change devices: `{\"devices\": [\"AA:BB:CC:DD:EE:01\"]}`\n- Watch all devices: `{\"devices\": []}`\n- Change URL: `{\"url\": \"https://example.com/new-webhook\"}`\n- Change matching logic: `{\"match\": \"all\", \"mode\": \"on_trigger\"}`\n\nSeveral fields can be changed in one request.","security":[{"ApiToken":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/WebhookInput"},"examples":{"delta":{"summary":"Switch to temperature-change notifications","value":{"mode":"on_delta","conditions":{"temperature_c":{"delta":1}}}},"pause":{"summary":"Pause the webhook","value":{"enabled":false}},"resume":{"summary":"Resume the webhook","value":{"enabled":true}},"threshold":{"summary":"Change temperature threshold","value":{"conditions":{"temperature_c":{"gt":30}}}},"devices":{"summary":"Replace the device filter","value":{"devices":["AA:BB:CC:DD:EE:01","AA:BB:CC:DD:EE:02"]}},"all_devices":{"summary":"Accept every device","value":{"devices":[]}},"destination":{"summary":"Change destination URL","value":{"url":"https://example.com/new-webhook"}},"telegram_message":{"summary":"Use measurement values in a Telegram message","value":{"url":"https://api.telegram.org/botYOUR_BOT_TOKEN/sendMessage?chat_id=YOUR_CHAT_ID&text=Temperature:%20{{temperature_c}}%20C%2C%20humidity:%20{{humidity_percent}}%25"}},"events":{"summary":"Change event filters","value":{"events":["wakeup","sleep"]}},"multiple_fields":{"summary":"Change several fields together","value":{"name":"Critical climate alert","match":"all","mode":"on_trigger","conditions":{"temperature_c":{"gte":30},"humidity_percent":{"gt":80}}}}}}}},"responses":{"200":{"description":"Updated webhook.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Webhook"},"example":{"id":"wh_4a219a3dd97245c6a46e901b","name":"High climate values","url":"https://example.com/webhook","devices":["78:1C:3C:21:4F:54"],"events":[],"match":"any","mode":"on_change","conditions":{"temperature_c":{"gt":28},"humidity_percent":{"gt":75}},"enabled":true,"last_delivery":null,"created_at":"2026-09-01T17:00:00Z","updated_at":"2026-09-01T17:00:00Z"}}}},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/TooManyAttempts"},"401":{"$ref":"#/components/responses/Unauthorized"},"422":{"$ref":"#/components/responses/ValidationError"}}},"delete":{"tags":["Webhooks"],"summary":"Delete a webhook","operationId":"deleteWebhook","description":"Stops future deliveries, cancels queued attempts, and removes the webhook from the list. No request body is required.","security":[{"ApiToken":[]}],"responses":{"200":{"description":"Webhook deleted.","content":{"application/json":{"example":{"deleted":true,"id":"wh_4a219a3dd97245c6a46e901b"}}}},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/TooManyAttempts"},"401":{"$ref":"#/components/responses/Unauthorized"},"422":{"$ref":"#/components/responses/ValidationError"}}}},"/api/v1/webhooks/{id}/test":{"post":{"tags":["Webhooks"],"summary":"Queue a test delivery","operationId":"testWebhook","security":[{"ApiToken":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","example":"wh_4a219a3dd97245c6a46e901b"}}],"responses":{"202":{"description":"Test queued.","content":{"application/json":{"example":{"queued":true,"delivery_id":"dlv_b90f805ac39249b0b84742bd"}}}},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/TooManyAttempts"},"401":{"$ref":"#/components/responses/Unauthorized"},"422":{"$ref":"#/components/responses/ValidationError"}}}},"/api/v1/webhooks/{id}/deliveries":{"get":{"tags":["Webhooks"],"summary":"List delivery attempts","operationId":"listWebhookDeliveries","security":[{"ApiToken":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","example":"wh_4a219a3dd97245c6a46e901b"}},{"name":"status","in":"query","required":false,"description":"Filter by delivery status.","schema":{"type":"string","enum":["pending","retrying","success","failed","cancelled"]}},{"name":"page","in":"query","required":false,"description":"Optional page number.","schema":{"type":"integer","minimum":1,"example":1}},{"name":"limit","in":"query","required":false,"description":"Optional page size, up to 100.","schema":{"type":"integer","minimum":1,"maximum":100,"example":20}}],"responses":{"200":{"description":"Delivery history.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/WebhookDeliveryList"},"example":{"webhook_id":"wh_4a219a3dd97245c6a46e901b","count":1,"data":[{"id":"dlv_b90f805ac39249b0b84742bd","event_type":"condition.triggered","status":"success","attempts":1,"http_status":204,"error":null,"created_at":"2026-09-01T17:05:00Z","delivered_at":"2026-09-01T17:05:01Z"}]}}}},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/TooManyAttempts"},"401":{"$ref":"#/components/responses/Unauthorized"},"422":{"$ref":"#/components/responses/ValidationError"}}}}},"components":{"securitySchemes":{"ApiToken":{"type":"apiKey","in":"header","name":"X-API-Token","description":"API token used to access organization data. Public demo token: `595a321cd5eedc5f6dd461d60a574719e142b5e53bee1a0e`."}},"schemas":{"CommandHistory":{"type":"object","required":["commands","type"],"properties":{"device":{"type":"string"},"type":{"type":"string","enum":["commands"]},"interval":{"type":"string","enum":["raw"]},"from":{"type":"string","format":"date-time"},"to":{"type":"string","format":"date-time"},"commands":{"type":"array","items":{"allOf":[{"$ref":"#/components/schemas/RelayControl/properties/command"},{"type":"object","properties":{"updated_at":{"type":"string","format":"date-time"}}}]}},"pagination":{"type":"object","properties":{"page":{"type":"integer"},"limit":{"type":"integer"},"total":{"type":"integer"},"has_next":{"type":"boolean"},"has_previous":{"type":"boolean"}}}}},"RelayControl":{"type":"object","properties":{"device":{"type":"string"},"reported_state":{"type":"string","enum":["on","off"]},"last_seen":{"type":"string","format":"date-time"},"command":{"type":"object","nullable":true,"properties":{"id":{"type":"integer"},"relay":{"type":"string","enum":["on","off"]},"status":{"type":"string","enum":["pending","sent","applied","expired","interrupted"]},"created_at":{"type":"string","format":"date-time"},"expires_at":{"type":"string","format":"date-time"}}}},"example":{"device":"AA:BB:CC:DD:EE:03","reported_state":"off","last_seen":"2026-09-12T18:00:00Z","command":{"id":1,"relay":"on","status":"pending","created_at":"2026-09-12T18:00:01Z","expires_at":"2026-09-12T18:01:01Z"}}},"Health":{"type":"object","required":["status","time","version","checks"],"properties":{"status":{"type":"string","enum":["ok","degraded"],"example":"ok"},"time":{"type":"string","format":"date-time"},"version":{"type":"string","example":"3.4.0"},"checks":{"type":"object","required":["api","database"],"properties":{"api":{"type":"object","required":["status"],"properties":{"status":{"type":"string","enum":["ok"]}}},"database":{"type":"object","required":["status","response_time_ms"],"properties":{"status":{"type":"string","enum":["ok","failed"]},"response_time_ms":{"type":"number","minimum":0,"description":"Duration of a live SELECT 1 query in milliseconds."}}},"webhook_queue":{"type":"object","required":["status","pending","failed"],"description":"Present only when the request includes a valid X-API-Token. Counts are restricted to that token's organization.","properties":{"status":{"type":"string","enum":["ok","failed"]},"pending":{"type":["integer","null"],"minimum":0,"description":"Deliveries waiting for an attempt, including retrying deliveries."},"failed":{"type":["integer","null"],"minimum":0,"description":"Deliveries that exhausted all retry attempts."}}}}}}},"Data":{"type":"object","description":"Available values depend on the device type.","properties":{"temperature_c":{"type":"number","example":24.2},"humidity_percent":{"type":"number","example":60},"battery_voltage_v":{"type":"number","example":2.53},"reboot_reason":{"type":"integer","description":"Device startup reason when supplied: 0 power-on, 1 reboot, 2 watchdog, 3 GPIO/button wakeup, 4 timer wakeup.","example":1},"relay_state":{"type":"string","enum":["on","off"],"description":"Reported relay state. Use interval=raw for history. Webhooks support eq on/off; use mode=on_change for ON and OFF notifications."},"contact_state":{"type":"string","enum":["open","closed"],"description":"Reported position of a door, gate or window contact. Not a lifecycle event."}},"additionalProperties":true},"Measurement":{"type":"object","properties":{"device":{"type":"string","example":"78:1C:3C:21:4F:54"},"device_type":{"type":"string","example":"climate"},"time":{"type":"string","format":"date-time"},"event":{"type":"string","description":"Optional lifecycle event. Omitted for ordinary measurements, including contact states.","enum":["boot","reboot","watchdog","crash","wakeup","sleep"],"example":"reboot"},"data":{"$ref":"#/components/schemas/Data"},"quality":{"$ref":"#/components/schemas/Quality"}}},"QualityIssue":{"type":"object","required":["field","severity","code","message"],"properties":{"field":{"type":"string","example":"temperature_c"},"severity":{"type":"string","enum":["suspicious","invalid"]},"code":{"type":"string","example":"outside_expected_range"},"message":{"type":"string","example":"Temperature is outside the usual -40 to 85 °C sensor range. Check the sensor driver and scaling."},"value":{"description":"Original reported value. Omitted from aggregated issue summaries."},"occurrences":{"type":"integer","description":"Number of matching issues in an aggregated interval."}}},"Quality":{"type":"object","required":["status","issues"],"description":"Advisory assessment that never changes or removes the original data.","properties":{"status":{"type":"string","enum":["valid","suspicious","invalid"]},"issues":{"type":"array","items":{"$ref":"#/components/schemas/QualityIssue"}},"flagged_samples":{"type":"integer","description":"Present for aggregated intervals."}}},"Device":{"type":"object","description":"A device and its most recently stored state.","properties":{"reported_state":{"type":"string","nullable":true,"enum":["on","off",null],"description":"Relay only: last reported channel state."},"command":{"$ref":"#/components/schemas/RelayControl/properties/command"},"organization":{"type":"string"},"mac":{"type":"string"},"device_type":{"type":"string"},"last_seen":{"type":"string","format":"date-time"},"latest":{"type":"object"}}},"DeviceList":{"type":"object","properties":{"organization":{"type":"string","example":"Public Demo Organization"},"devices":{"type":"array","items":{"$ref":"#/components/schemas/Device"}}}},"MeasurementCollection":{"type":"object","properties":{"device":{"type":"string"},"device_type":{"type":["string","null"]},"from":{"type":"string","format":"date-time"},"to":{"type":"string","format":"date-time"},"interval":{"type":"string","enum":["raw","15m","1h","6h","1d"]},"count":{"type":"integer"},"data":{"type":"array","items":{"type":"object"}},"pagination":{"type":"object","description":"Present only when interval=raw.","properties":{"page":{"type":"integer"},"limit":{"type":"integer"},"total_records":{"type":"integer"},"total_pages":{"type":"integer"},"has_previous":{"type":"boolean"},"has_next":{"type":"boolean"}}}}},"UpdateCollection":{"type":"object","required":["device","device_type","count","updates","last_id","has_more"],"properties":{"device":{"type":"string","example":"78:1C:3C:21:4F:54"},"device_type":{"type":"string","example":"climate"},"count":{"type":"integer","minimum":0},"updates":{"type":"array","items":{"type":"object","required":["id","time","data","quality"],"properties":{"id":{"type":"integer","minimum":1,"description":"Sequential record ID local to this organization and device. Starts at 1 and increases by one for every stored record."},"time":{"type":"string","format":"date-time"},"event":{"type":"string","enum":["boot","reboot","watchdog","crash","wakeup","sleep"],"description":"Present only for lifecycle events."},"data":{"$ref":"#/components/schemas/Data"},"quality":{"$ref":"#/components/schemas/Quality"}}}},"last_id":{"type":"integer","description":"Save this value and send it as after_id on the next request."},"has_more":{"type":"boolean","description":"True when another batch is already available after last_id."}}},"EventCollection":{"type":"object","properties":{"device":{"type":"string"},"device_type":{"type":["string","null"]},"events":{"type":"array","items":{"type":"object","properties":{"event":{"type":"string","enum":["boot","reboot","watchdog","crash","wakeup","sleep"]},"data":{"$ref":"#/components/schemas/Data"},"time":{"type":"string","format":"date-time"}}}}}},"Pagination":{"type":"object","properties":{"page":{"type":"integer"},"limit":{"type":"integer"},"total_records":{"type":"integer"},"total_pages":{"type":"integer"},"has_previous":{"type":"boolean"},"has_next":{"type":"boolean"}}},"WebhookConditions":{"type":"object","description":"Metric names mapped to rules. Operators: gt, gte, lt, lte and eq. For mode=on_delta, each metric must have exactly one delta rule with a finite positive number. Delta measures absolute change in either direction from its baseline; do not mix it with threshold operators.","additionalProperties":{"type":"object","additionalProperties":{"type":["number","string","boolean","null"]}},"example":{"temperature_c":{"gt":28},"humidity_percent":{"gt":75}}},"WebhookInput":{"type":"object","properties":{"name":{"type":"string","maxLength":100},"url":{"type":"string","format":"uri","description":"Public HTTPS URL on port 443. May contain supported `{{value}}` placeholders."},"devices":{"type":"array","maxItems":100,"description":"Empty means all devices.","items":{"type":"string"}},"events":{"type":"array","default":[],"description":"Empty or omitted on creation: ordinary measurements only, including contact states. Non-empty: only listed lifecycle events. On update, omission preserves the existing filter; [] restores ordinary measurements.","items":{"type":"string","enum":["boot","reboot","watchdog","crash","wakeup","sleep"]}},"match":{"type":"string","enum":["any","all"],"default":"any"},"mode":{"type":"string","enum":["on_change","on_trigger","every_match","on_delta"]},"conditions":{"$ref":"#/components/schemas/WebhookConditions"},"enabled":{"type":"boolean","default":true}}},"Webhook":{"allOf":[{"$ref":"#/components/schemas/WebhookInput"},{"type":"object","properties":{"id":{"type":"string"},"secret":{"type":"string","description":"One-time HMAC signing secret. Store it securely and use it to verify `X-Webhook-Signature`; it cannot be retrieved again from list or get responses."},"last_delivery":{"type":["object","null"]},"created_at":{"type":"string","format":"date-time"},"updated_at":{"type":"string","format":"date-time"}}}]},"WebhookList":{"type":"object","properties":{"count":{"type":"integer"},"data":{"type":"array","items":{"$ref":"#/components/schemas/Webhook"}},"pagination":{"$ref":"#/components/schemas/Pagination"}}},"WebhookDeliveryList":{"type":"object","properties":{"webhook_id":{"type":"string"},"count":{"type":"integer"},"data":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"event_type":{"type":"string"},"status":{"type":"string"},"attempts":{"type":"integer"},"http_status":{"type":["integer","null"]},"error":{"type":["string","null"]},"created_at":{"type":"string","format":"date-time"},"delivered_at":{"type":["string","null"],"format":"date-time"}}}},"pagination":{"$ref":"#/components/schemas/Pagination"}}},"Error":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string"},"message":{"type":"string"}}}}}},"responses":{"TooManyAttempts":{"description":"Token authentication from this IP is temporarily blocked after more than 15 failed attempts within 15 minutes. The block lasts 15 minutes.","headers":{"Retry-After":{"description":"Seconds remaining before retrying.","schema":{"type":"integer","example":900}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":{"code":"too_many_attempts","message":"Too many failed authentication attempts. Try again later."}}}}},"Unauthorized":{"description":"The API token is missing or invalid.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"missing_token":{"summary":"Token was not supplied","value":{"error":{"code":"unauthorized","message":"X-API-Token is required."}}},"invalid_token":{"summary":"Token is not recognized","value":{"error":{"code":"unauthorized","message":"Invalid API token."}}}}}}},"ValidationError":{"description":"The request parameters are invalid.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"missing_device":{"summary":"Device was not selected","value":{"error":{"code":"validation_error","message":"Missing device parameter."}}},"incomplete_range":{"summary":"Only one exact boundary was supplied","value":{"error":{"code":"validation_error","message":"from and to must be provided together."}}},"invalid_period":{"summary":"Relative period has the wrong format","value":{"error":{"code":"validation_error","message":"period must use the format 24h, 5d, 3m or 1y."}}},"invalid_interval":{"summary":"Aggregation interval is unsupported","value":{"error":{"code":"validation_error","message":"interval must be: raw, 15m, 1h, 6h or 1d."}}}}}}},"NotFound":{"description":"The requested device or data was not found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"device":{"summary":"Device does not exist","value":{"error":{"code":"not_found","message":"Device not found."}}},"measurement":{"summary":"Device has no measurements","value":{"error":{"code":"not_found","message":"No measurements found."}}}}}}}}}}