{"openapi":"3.1.0","info":{"title":"DIENAS API","version":"4.0.0","description":"**Distributed IoT Events, Notifications & Automation Service**\n\nAPI documentation for devices, incremental updates, measurements, events and webhooks.\n\nConnect sensors and relays to your own business software, without relying on the Tuya cloud. [Discover DIENAS for business](/en).\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\",\"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\",\"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\nRaw relay readings include `source` alongside `data`. It describes the origin of the stored transition, not the current connection status:\n\n| Source | Meaning |\n| --- | --- |\n| `button` | Physical button press processed by the device |\n| `switch` | External switch input (GPIO14 on the test relay); available with a script that reports this source |\n| `api` | Server command applied and reported by the device |\n| `boot` | Initial OFF state after device startup, including a restart |\n| `unknown` | Old records or unclassified changes, including the device web interface and external toggle input |\n\nYou can read it in `latest.source` from `GET /api/v1/devices/{mac}`, in `data[].source` from raw measurements, and in `updates[].source` from device updates. Climate and Contact omit this field. Numeric aggregation has no single source. There is currently no `source` query filter or webhook condition: filter returned readings or received webhook payloads in your application.\n\n### Local button versus remote command\n\nIn Dashboard, **Relay history** shows reported ON/OFF states with their origin (Button, API, Startup, Unknown), newest first with page-size selection and Developer tools. **API command history** is separate: it shows remote requests and their pending/applied/error statuses. Do not merge requests into the state timeline: an unexecuted or same-state command is not a new physical transition. The state timeline includes only reports received and stored by the server; it cannot reconstruct lost device events.\n\n1. **Physical button:** the device records the press, switches its output in a separate input-processing pass, and queues the resulting state for prompt reporting. It does not wait for the normal 10-second poll.\n2. **Remote command:** POST an `on` or `off` command. HTTP 202 means queued, not applied. The device retrieves it during its next poll, applies it, then schedules a confirmation promptly, normally after 1 second rather than another 10 seconds.\n3. **Idle:** the device polls approximately every 10 seconds after a successful response. Unchanged polls update `last_seen`, not measurement history.\n\nThese are scheduling targets, not latency guarantees: queued transitions, network delays and retry backoff can delay delivery. `applied` confirms that device software processed the command, not that the electrical load was verified by a separate sensor. A same-state command is acknowledged without inserting a duplicate reading; therefore `latest.source` can remain the source of an earlier transition. A physical button press is not an API command and does not create a Command history entry.\n\nExample transitions from `GET /api/v1/measurements?device=AA:BB:CC:DD:EE:03&interval=raw`:\n```json\n{\"data\":[{\"time\":\"2026-09-21T10:00:00Z\",\"source\":\"button\",\"data\":{\"relay_state\":\"on\"}},{\"time\":\"2026-09-21T10:15:00Z\",\"source\":\"api\",\"data\":{\"relay_state\":\"off\"}}]}\n```\nThis is a shortened example. `time` is server receipt time; queued changes may arrive later or share a timestamp. Preserve response order. The device sends captured changes promptly and polls while idle. Its finite RAM queue is not a durable event log: power loss, overflow or network/firmware failures can lose events.\n\nThe current relay script buffers 16 state transitions plus one in-flight report. Pending button presses are counted separately. Multiple external/web-interface changes between input-processing passes may be missed. On startup the relay defaults to OFF; it does not restore the previous ON state.\n\n### Relay webhook origin\n\nUse the existing ON/OFF webhook example (`relay_state.eq=on`, `mode=on_change`) to receive trigger/recovery notifications. Relay deliveries include top-level `source`, outside `data`, for example:\n```json\n{\"id\":\"evt_example\",\"type\":\"condition.triggered\",\"time\":\"2026-09-21T10:00:00Z\",\"device\":{\"mac\":\"AA:BB:CC:DD:EE:03\",\"type\":\"relay\"},\"source\":\"button\",\"data\":{\"relay_state\":\"on\"}}\n```\nThis shortened payload omits condition details. To react only to physical-button changes, check `source === \"button\"` in your receiving service. Delivery is queued; it is not an instantaneous acknowledgement to the device.\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\",\"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## Device availability: online and offline\n\n`GET /api/v1/devices` and `GET /api/v1/devices/{mac}` include `status.availability`. The server calculates it at request time using `last_seen`, independently of measurement quality.\n\n| Availability | Meaning |\n| --- | --- |\n| `online` | The server received a device report within the last 24 hours (including exactly 24 hours ago). This is not proof of a live connection: battery devices normally sleep between reports. |\n| `offline` | More than 24 hours (86,400 seconds) have passed since the last report. `status.issues` includes `device_offline`. |\n| `unknown` | A usable last-contact timestamp is unavailable. |\n\nThe 24-hour threshold is the same for Climate, Contact and Relay. Climate normally reports every five minutes. Updated Contact devices report changes and send a periodic `checkin` after six hours of sleep even if the door has not moved. Relay polls update `last_seen` even when the output stays unchanged and no new measurement is stored. An old last state change alone does not make a relay offline.\n\nFor example, if the last report arrived on September 22 at 10:00 UTC, the device becomes offline after September 23 at 10:00 UTC. After another successfully processed report, the next device API response shows online again.\n\n```json\n\"status\": {\n  \"availability\": \"offline\",\n  \"issues\": [{\n    \"code\": \"device_offline\",\n    \"message\": \"No contact with the server for over 24 hours.\"\n  }]\n}\n```\n\nOffline indicates missing communication, not a confirmed battery or hardware failure: power, Wi-Fi or server connectivity may be responsible. The last readings remain available. Battery, temperature and humidity warnings remain in `latest.quality`; they are not duplicated in `status`. Historical measurement quality does not change just because the device is now offline.\n\n### Offline and recovery webhooks\n\nSubscribe with `events: [\"offline\", \"online\"]`, `mode: \"every_match\"`, and empty `conditions: {}`. Despite the mode name, these events occur only on a change of availability, not on every reading or scheduled check. Select just `offline` or `online` if desired. `devices: []` includes all devices in your organization.\n\nThe hosting scheduler checks hourly: an offline notification is normally queued between 24 and 25 hours after the last report. Recovery is queued immediately when the API saves the first report after a recorded offline state; it does not wait for cron. First-ever device contact does not send a recovery message. An outage that ends before a scheduled check records offline does not produce an offline/online pair. New or re-enabled subscriptions do not replay already recorded transitions. When monitoring is first enabled, existing stale devices are evaluated by the first check.\n\nAvailability messages reuse the delivery queue, retries, and delivery history. No synthetic measurements are inserted, and `last_seen` is never advanced by cron. These server-detected transitions are not firmware events in `/api/v1/events`. They appear in webhook delivery history. Retries can result in duplicate delivery if a receiver processes a request but its response is lost; receivers should deduplicate by payload `id`.\n\n```json\n{\"url\":\"https://example.com/notify?status={{event}}\",\"devices\":[],\"events\":[\"offline\",\"online\"],\"mode\":\"every_match\",\"conditions\":{}}\n```\n\nExample payload saved in delivery history (the destination is called by GET; URL placeholders carry the selected values):\n\n```json\n{\n  \"id\": \"evt_example\",\n  \"type\": \"device.availability\",\n  \"event\": \"offline\",\n  \"time\": \"2026-09-24T12:00:00Z\",\n  \"device\": {\"mac\":\"78:1C:3C:21:4F:54\",\"type\":\"climate\"},\n  \"data\": {},\n  \"status\": {\"availability\":\"offline\",\"previous\":\"online\"},\n  \"last_seen\": \"2026-09-23T11:30:00Z\"\n}\n```\n\nUse `{{event}}` for `offline` / `online`, `{{device.mac}}` for the device and `{{time}}` for detection time. Measurement placeholders are empty for availability events. Numeric conditions and delta modes cannot be combined with availability subscriptions. The protected scheduler URL is an operator-only endpoint, not an organization API-token endpoint.\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: `{{data.temperature_c}}`, `{{data.humidity_percent}}`, `{{data.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 measurement examples do not imply cooldowns, hysteresis or scheduled summaries. For offline detection use separate availability event subscriptions above. 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 `{{data.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\nFor POST deliveries the 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. For GET deliveries there is no body: sign `timestamp + \".\" + request_target` instead, where request_target is the exact encoded path and query string, such as `/notify?state=open`. Do not decode, reorder or rebuild query parameters. In PHP use `$_SERVER['REQUEST_URI']`. The receiver should allow only its configured HTTP method. The examples below verify POST JSON; for GET replace the raw body input with the request target and do not parse JSON.\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.\n\n### Device details: name, location and description\nDevices of any type can have an optional `name`, stored on the server. MAC remains the unique identifier within your organization; duplicate names are allowed. No firmware update, device wake-up or relay command is involved.\n\nSend `POST /api/v1/devices/{mac}` with any combination of `name` (up to 80 characters), `location` (160) and `description` (2,000). All are optional, trimmed Unicode strings. Description supports line breaks and tabs; other control characters are rejected. Omitted fields remain unchanged. Set a field to null or an empty string to clear it. Send metadata separately from relay commands; mixing them returns 422 and changes nothing. Unknown devices, including those outside your token's organization, return 404. No firmware changes are needed.\n\nExamples: `{\"location\":\"Office · First floor\"}` changes only location; `{\"description\":\"Near the entrance.\\nInstalled September 2026.\"}` updates notes; `{\"location\":null,\"description\":null}` clears both while preserving the name. GET device/list responses include all three fields next to MAC, with null for unset values. History responses keep only the current name to avoid repeating long metadata.\n\n```bash\ncurl -X POST \"$BASE_URL/api/v1/devices/78:1C:3C:21:4F:54\" \\\n  -H \"X-API-Token: $API_TOKEN\" -H \"Content-Type: application/json\" \\\n  --data '{\"name\":\"Bathroom\"}'\n# Response (if other fields are unset): {\"mac\":\"78:1C:3C:21:4F:54\",\"name\":\"Bathroom\",\"location\":null,\"description\":null}\n```\n\nGET device/list responses include `name` immediately after `mac` (null when unset). GET measurements also returns the current `name` immediately after `device`, for raw readings, aggregated readings and command history, including empty results. The name is not repeated in each history row and is not a historical snapshot: renaming changes this top-level label even when querying old readings. New webhook payloads include `device.name`; `{{device.name}}` in destination URLs uses the name, falling back to MAC. Values are URL-encoded automatically. Already queued deliveries retain the name captured when queued. Renaming does not trigger webhooks. Test deliveries use an example name.\n\n### Outgoing HTTP method\n`method` selects how notifications are delivered: `POST` (default) sends the standard event JSON body with `Content-Type: application/json`; `GET` sends no body, so include needed values in the destination URL using placeholders. Both require a public HTTPS destination and support URL placeholders, signatures, delivery history and retries. Creating or editing a webhook still uses POST to this API regardless of the outgoing method.\n\nExisting webhooks continue to use POST. Omitting method on update preserves it. `POST /webhooks/{id}/test` uses the saved method and synthetic example data. Delivery history `payload` is the event context; for GET it is not transmitted as a body. Pending deliveries and retries use the webhook's current URL and method at send time. Receivers must tolerate duplicate deliveries and use X-Webhook-Id for deduplication. Any 2xx response confirms success; redirects are not followed.\n\nPOST example configuration: `{\"url\":\"https://example.com/webhook\",\"method\":\"POST\",\"devices\":[\"78:1C:3C:21:4F:54\"],\"conditions\":{\"temperature_c\":{\"gt\":25}}}`.\n\nGET example configuration: `{\"url\":\"https://example.com/notify?device={{device.mac}}&temperature={{data.temperature_c}}\",\"method\":\"GET\",\"devices\":[\"78:1C:3C:21:4F:54\"],\"conditions\":{\"temperature_c\":{\"gt\":25}}}`.\n\nTo change only delivery method, send `{\"method\":\"POST\"}` to `POST /api/v1/webhooks/{id}`. POST sends the standard event payload unless a custom `body` object is configured. Custom headers are not supported. To switch a custom-body webhook to GET, send `{\"method\":\"GET\",\"body\":null}`.\n\n### Custom POST body\nUse `body` to send your own JSON object, for example `{\"temperature\":\"{{data.temperature_c}}\",\"message\":\"Device {{device.mac}}\"}`. It replaces the entire standard JSON body. The same placeholders as destination URLs work in JSON values, including `{{data.contact_state}}`, `{{data.relay_state}}`, `{{delta.temperature_c}}` and `{{previous.temperature_c}}`. Keys cannot contain placeholders. Nested objects and arrays are supported. Templates are limited to 16 KiB and 12 nesting levels; rendered bodies are limited to 64 KiB.\n\nA placeholder occupying the entire string preserves its JSON type: `\"{{data.temperature_c}}\"` becomes a number. Missing whole-value placeholders become null; missing placeholders inside text become empty strings. Embedded values become text. JSON escaping happens after substitution; never URL-encode body values. Substitution is single-pass. An empty object sends `{}`; `body:null` restores the standard event JSON. Omission on update preserves the template. GET cannot have a custom body.\n\nSignatures cover the exact rendered POST body. Delivery history `request_body` contains that JSON text for the latest recorded attempt, not the template, and remains unchanged when webhook settings are edited. It is null for GET, older history or when no body was captured. Retries use the current webhook configuration and replace this latest-attempt snapshot. The saved event payload itself is retained separately; custom bodies do not change condition evaluation.\n\nTelegram example: `{\"url\":\"https://api.telegram.org/botYOUR_BOT_TOKEN/sendMessage\",\"method\":\"POST\",\"body\":{\"chat_id\":\"YOUR_CHAT_ID\",\"text\":\"Door: {{data.contact_state}}\\nDevice: {{device.mac}}\"}}`. Use this with your device filter and conditions. A test uses synthetic example values and really sends a notification.\n\n### Device metadata placeholders\nUse `{{device.location}}` and `{{device.description}}` in destination URLs (GET or POST) and custom POST body values. They use device metadata, not measurement fields. Values are captured when the event is queued; later edits do not rewrite queued events. Supported for ordinary, delta and availability events. URL substitutions are URL-encoded automatically. Missing metadata becomes an empty string in URLs or embedded text, and null for whole-value JSON placeholders. Standard webhook JSON includes device.location and device.description; test notifications use example metadata. Example custom body: `{\"room\":\"{{device.location}}\",\"notes\":\"{{device.description}}\",\"message\":\"Door at {{device.location}}: {{data.contact_state}}\"}`."},"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`checks.availability_cron` is public and contains no organization data or secrets. It tracks completion of the offline check, not delivery of every queued webhook. `ok`: last successful run within 90 minutes; `warning`: no success for over 90 minutes (or a first run started over 90 minutes ago without completion); `error`: last recorded run failed or tracking cannot be read; `unknown`: no run/success recorded yet. A successful check with zero transitions still counts. `next_expected_at` estimates one hour after the last success; the actual schedule is configured on the hosting server. Cron warnings/errors alone do not change the overall API status or HTTP 200. Database failures still return HTTP 503; cron status is unknown when its database cannot be read. Failed processes that cannot record an error are detected by stale success time.\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":"4.0.0","checks":{"api":{"status":"ok"},"database":{"status":"ok","response_time_ms":4},"availability_cron":{"status":"ok","last_started_at":"2026-09-06T20:00:00Z","last_success_at":"2026-09-06T20:00:01Z","last_failed_at":null,"next_expected_at":"2026-09-06T21:00:01Z"}}}},"authenticated":{"summary":"Organization-scoped queue information with a valid token","value":{"status":"ok","time":"2026-09-06T20:15:00Z","version":"4.0.0","checks":{"api":{"status":"ok"},"database":{"status":"ok","response_time_ms":4},"availability_cron":{"status":"ok","last_started_at":"2026-09-06T20:00:00Z","last_success_at":"2026-09-06T20:00:01Z","last_failed_at":null,"next_expected_at":"2026-09-06T21:00:01Z"},"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":"4.0.0","checks":{"api":{"status":"ok"},"database":{"status":"failed","response_time_ms":1000.25},"availability_cron":{"status":"unknown","last_started_at":null,"last_success_at":null,"last_failed_at":null,"next_expected_at":null}}}},"authenticated":{"summary":"Database check failed with authentication","value":{"status":"degraded","time":"2026-09-06T20:15:00Z","version":"4.0.0","checks":{"api":{"status":"ok"},"database":{"status":"failed","response_time_ms":1000.25},"availability_cron":{"status":"unknown","last_started_at":null,"last_success_at":null,"last_failed_at":null,"next_expected_at":null},"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\nEach device includes `status.availability`: online when last seen within 24 hours, offline after more than 24 hours without a report, or unknown when last contact is unavailable. Sleeping devices can be online; unchanged relay polls count as contact. Measurement warnings remain in `latest.quality`. See **Device availability: online and offline** in the introduction.\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```\n\n### Device details: name, location and description\nDevices of any type can have an optional `name`, stored on the server. MAC remains the unique identifier within your organization; duplicate names are allowed. No firmware update, device wake-up or relay command is involved.\n\nSend `POST /api/v1/devices/{mac}` with any combination of `name` (up to 80 characters), `location` (160) and `description` (2,000). All are optional, trimmed Unicode strings. Description supports line breaks and tabs; other control characters are rejected. Omitted fields remain unchanged. Set a field to null or an empty string to clear it. Send metadata separately from relay commands; mixing them returns 422 and changes nothing. Unknown devices, including those outside your token's organization, return 404. No firmware changes are needed.\n\nExamples: `{\"location\":\"Office · First floor\"}` changes only location; `{\"description\":\"Near the entrance.\\nInstalled September 2026.\"}` updates notes; `{\"location\":null,\"description\":null}` clears both while preserving the name. GET device/list responses include all three fields next to MAC, with null for unset values. History responses keep only the current name to avoid repeating long metadata.\n\n```bash\ncurl -X POST \"$BASE_URL/api/v1/devices/78:1C:3C:21:4F:54\" \\\n  -H \"X-API-Token: $API_TOKEN\" -H \"Content-Type: application/json\" \\\n  --data '{\"name\":\"Bathroom\"}'\n# Response (if other fields are unset): {\"mac\":\"78:1C:3C:21:4F:54\",\"name\":\"Bathroom\",\"location\":null,\"description\":null}\n```\n\nGET device/list responses include `name` immediately after `mac` (null when unset). GET measurements also returns the current `name` immediately after `device`, for raw readings, aggregated readings and command history, including empty results. The name is not repeated in each history row and is not a historical snapshot: renaming changes this top-level label even when querying old readings. New webhook payloads include `device.name`; `{{device.name}}` in destination URLs uses the name, falling back to MAC. Values are URL-encoded automatically. Already queued deliveries retain the name captured when queued. Renaming does not trigger webhooks. Test deliveries use an example name.\n\n### Device timing settings\nSend `POST /api/v1/devices/{mac}` containing `settings`, optionally together with `name`, `location` and `description`. The edit is atomic: an invalid field prevents all changes. The settings object must contain exactly the field supported by this device type:\n\n| Type | Field | Allowed seconds | Default in the current script |\n| --- | --- | --- | --- |\n| Climate | `sleep_seconds` | 180–21600 | 300 |\n| Contact | `awake_after_change_seconds` | 5–300 | 15 for DS100, 10 for RH-MAGv3B |\n| Relay | `poll_interval_seconds` | 5–3600 | 10 |\n\nExamples: `{\"settings\":{\"sleep_seconds\":600}}`, `{\"settings\":{\"awake_after_change_seconds\":15}}`, `{\"settings\":{\"poll_interval_seconds\":30}}`.\n\nValues must be JSON integers, not strings. Fields for another device type, empty settings, unknown fields and edits mixed with relay commands return 422 without changing anything. Devices outside your organization return 404. Omitted metadata or settings remain unchanged.\n\nFor example: `{\"name\":\"Entrance\",\"location\":\"Ground floor\",\"description\":\"Main entrance\",\"settings\":{\"awake_after_change_seconds\":15}}` saves all four fields in one request.\n\nThe short `z` parameter is only used by the device transport (`/i`) to save URL space. Public JSON uses descriptive names in `data`: `sleep_seconds` for Climate, `awake_after_change_seconds` for Contact, and `poll_interval_seconds` for Relay. These are the intervals reported with that reading, in seconds, not the latest requested settings. `settings.desired` and `settings.reported` describe the current configuration exchange. Historical readings retain their recorded interval; older stored `z` fields are presented using the same descriptive names.\n\nSettings are stored on the server and delivered on the device's next request. This does not wake a sleeping device. Updated BAT scripts are required: older scripts continue working but cannot apply timing settings. GET device and device-list responses include `settings.desired`, `settings.reported` and `settings.status`. `not_configured` means no remote override has been saved. `pending` means the latest report does not yet confirm the desired value. `applied` means the device has reported that value; merely sending a reply does not confirm application. `reported` is null until a compatible script reports its interval. Confirmation can arrive at the following normal report; no extra battery-powered report is forced.\n\nThe script stores the received setting as a startup value and uses it after reboot. Until a first successful configuration exchange it uses its local default. The relay still starts OFF after power-up. Relay timing controls only idle polling for remote commands: button/switch changes and command acknowledgements are sent promptly. Climate timing is deep-sleep duration; wake-up and connection time add to the actual interval between readings. Contact timing is its inactivity window, renewed after changes; queued/unconfirmed data may keep it awake longer. Its six-hour check-in and five-minute service-mode limit are unchanged. Increasing an interval increases remote-command latency or delays readings; shortening battery-device intervals increases battery consumption.\n\n```bash\ncurl -X POST \"$BASE_URL/api/v1/devices/78:1C:3C:21:4F:54\" \\\n  -H \"X-API-Token: $API_TOKEN\" -H \"Content-Type: application/json\" \\\n  --data '{\"settings\":{\"sleep_seconds\":600}}'\n# {\"mac\":\"78:1C:3C:21:4F:54\",\"settings\":{\"desired\":{\"sleep_seconds\":600},\"reported\":null,\"status\":\"pending\"}}\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","name":"Bathroom","location":"Office","description":null,"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":[]}},"status":{"availability":"online","issues":[]},"settings":{"desired":null,"reported":null,"status":"not_configured"}},{"mac":"64:64:4A:3E:04:3E","name":"Bathroom","location":"Office","description":null,"type":"climate","last_seen":"2026-08-30T06:29:59Z","latest":{"data":{"temperature_c":24.1,"humidity_percent":60},"quality":{"status":"valid","issues":[]}},"status":{"availability":"offline","issues":[{"code":"device_offline","message":"No contact with the server for over 24 hours."}]},"settings":{"desired":null,"reported":null,"status":"not_configured"}}]}}}},"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":"updateDevice","summary":"Edit device details, timing settings or request relay on/off","security":[{"ApiToken":[]}],"description":"### Device details: name, location and description\nDevices of any type can have an optional `name`, stored on the server. MAC remains the unique identifier within your organization; duplicate names are allowed. No firmware update, device wake-up or relay command is involved.\n\nSend `POST /api/v1/devices/{mac}` with any combination of `name` (up to 80 characters), `location` (160) and `description` (2,000). All are optional, trimmed Unicode strings. Description supports line breaks and tabs; other control characters are rejected. Omitted fields remain unchanged. Set a field to null or an empty string to clear it. Send metadata separately from relay commands; mixing them returns 422 and changes nothing. Unknown devices, including those outside your token's organization, return 404. No firmware changes are needed.\n\nExamples: `{\"location\":\"Office · First floor\"}` changes only location; `{\"description\":\"Near the entrance.\\nInstalled September 2026.\"}` updates notes; `{\"location\":null,\"description\":null}` clears both while preserving the name. GET device/list responses include all three fields next to MAC, with null for unset values. History responses keep only the current name to avoid repeating long metadata.\n\n```bash\ncurl -X POST \"$BASE_URL/api/v1/devices/78:1C:3C:21:4F:54\" \\\n  -H \"X-API-Token: $API_TOKEN\" -H \"Content-Type: application/json\" \\\n  --data '{\"name\":\"Bathroom\"}'\n# Response (if other fields are unset): {\"mac\":\"78:1C:3C:21:4F:54\",\"name\":\"Bathroom\",\"location\":null,\"description\":null}\n```\n\nGET device/list responses include `name` immediately after `mac` (null when unset). GET measurements also returns the current `name` immediately after `device`, for raw readings, aggregated readings and command history, including empty results. The name is not repeated in each history row and is not a historical snapshot: renaming changes this top-level label even when querying old readings. New webhook payloads include `device.name`; `{{device.name}}` in destination URLs uses the name, falling back to MAC. Values are URL-encoded automatically. Already queued deliveries retain the name captured when queued. Renaming does not trigger webhooks. Test deliveries use an example name.\n\n### Relay commands\n\nCreates one command for an already registered relay in the token's organization. HTTP 202 means queued, NOT executed. Idle polling runs about every 10 seconds after the previous successful response. Updated devices schedule an applied-command confirmation promptly (normally after 1 second), including commands that request the already-current state. Same-state confirmations do not add duplicate measurements. Physical-button changes also report promptly but are not command-history entries. Polling pauses on a transport timeout; delivery is not guaranteed. Check command.status for acknowledgement and reported_state for the last reported output; applied is not independent electrical feedback.\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–3600). 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.\n\n### Device timing settings\nSend `POST /api/v1/devices/{mac}` containing `settings`, optionally together with `name`, `location` and `description`. The edit is atomic: an invalid field prevents all changes. The settings object must contain exactly the field supported by this device type:\n\n| Type | Field | Allowed seconds | Default in the current script |\n| --- | --- | --- | --- |\n| Climate | `sleep_seconds` | 180–21600 | 300 |\n| Contact | `awake_after_change_seconds` | 5–300 | 15 for DS100, 10 for RH-MAGv3B |\n| Relay | `poll_interval_seconds` | 5–3600 | 10 |\n\nExamples: `{\"settings\":{\"sleep_seconds\":600}}`, `{\"settings\":{\"awake_after_change_seconds\":15}}`, `{\"settings\":{\"poll_interval_seconds\":30}}`.\n\nValues must be JSON integers, not strings. Fields for another device type, empty settings, unknown fields and edits mixed with relay commands return 422 without changing anything. Devices outside your organization return 404. Omitted metadata or settings remain unchanged.\n\nFor example: `{\"name\":\"Entrance\",\"location\":\"Ground floor\",\"description\":\"Main entrance\",\"settings\":{\"awake_after_change_seconds\":15}}` saves all four fields in one request.\n\nThe short `z` parameter is only used by the device transport (`/i`) to save URL space. Public JSON uses descriptive names in `data`: `sleep_seconds` for Climate, `awake_after_change_seconds` for Contact, and `poll_interval_seconds` for Relay. These are the intervals reported with that reading, in seconds, not the latest requested settings. `settings.desired` and `settings.reported` describe the current configuration exchange. Historical readings retain their recorded interval; older stored `z` fields are presented using the same descriptive names.\n\nSettings are stored on the server and delivered on the device's next request. This does not wake a sleeping device. Updated BAT scripts are required: older scripts continue working but cannot apply timing settings. GET device and device-list responses include `settings.desired`, `settings.reported` and `settings.status`. `not_configured` means no remote override has been saved. `pending` means the latest report does not yet confirm the desired value. `applied` means the device has reported that value; merely sending a reply does not confirm application. `reported` is null until a compatible script reports its interval. Confirmation can arrive at the following normal report; no extra battery-powered report is forced.\n\nThe script stores the received setting as a startup value and uses it after reboot. Until a first successful configuration exchange it uses its local default. The relay still starts OFF after power-up. Relay timing controls only idle polling for remote commands: button/switch changes and command acknowledgements are sent promptly. Climate timing is deep-sleep duration; wake-up and connection time add to the actual interval between readings. Contact timing is its inactivity window, renewed after changes; queued/unconfirmed data may keep it awake longer. Its six-hour check-in and five-minute service-mode limit are unchanged. Increasing an interval increases remote-command latency or delays readings; shortening battery-device intervals increases battery consumption.\n\n```bash\ncurl -X POST \"$BASE_URL/api/v1/devices/78:1C:3C:21:4F:54\" \\\n  -H \"X-API-Token: $API_TOKEN\" -H \"Content-Type: application/json\" \\\n  --data '{\"settings\":{\"sleep_seconds\":600}}'\n# {\"mac\":\"78:1C:3C:21:4F:54\",\"settings\":{\"desired\":{\"sleep_seconds\":600},\"reported\":null,\"status\":\"pending\"}}\n```","requestBody":{"required":true,"content":{"application/json":{"schema":{"oneOf":[{"type":"object","minProperties":1,"additionalProperties":false,"properties":{"name":{"type":"string","nullable":true,"maxLength":80},"location":{"type":"string","nullable":true,"maxLength":160},"description":{"type":"string","nullable":true,"maxLength":2000}}},{"type":"object","required":["relay"],"properties":{"relay":{"type":"string","enum":["on","off"]},"expires_in":{"type":"integer","minimum":10,"maximum":3600,"default":60}},"additionalProperties":false},{"type":"object","required":["settings"],"additionalProperties":false,"properties":{"settings":{"type":"object","required":["sleep_seconds"],"additionalProperties":false,"properties":{"sleep_seconds":{"type":"integer","minimum":180,"maximum":21600}}},"name":{"type":"string","nullable":true,"maxLength":80},"location":{"type":"string","nullable":true,"maxLength":160},"description":{"type":"string","nullable":true,"maxLength":2000}}},{"type":"object","required":["settings"],"additionalProperties":false,"properties":{"settings":{"type":"object","required":["awake_after_change_seconds"],"additionalProperties":false,"properties":{"awake_after_change_seconds":{"type":"integer","minimum":5,"maximum":300}}},"name":{"type":"string","nullable":true,"maxLength":80},"location":{"type":"string","nullable":true,"maxLength":160},"description":{"type":"string","nullable":true,"maxLength":2000}}},{"type":"object","required":["settings"],"additionalProperties":false,"properties":{"settings":{"type":"object","required":["poll_interval_seconds"],"additionalProperties":false,"properties":{"poll_interval_seconds":{"type":"integer","minimum":5,"maximum":3600}}},"name":{"type":"string","nullable":true,"maxLength":80},"location":{"type":"string","nullable":true,"maxLength":160},"description":{"type":"string","nullable":true,"maxLength":2000}}}]},"examples":{"details":{"summary":"Set all device details","value":{"name":"Entrance door","location":"Office · Ground floor","description":"Main entrance sensor."}},"locationOnly":{"summary":"Change location, keep name and description","value":{"location":"Warehouse"}},"descriptionOnly":{"summary":"Update multiline notes","value":{"description":"Near the entrance.\nInstalled September 2026."}},"clearDetails":{"summary":"Clear location and notes, keep name","value":{"location":null,"description":null}},"rename":{"summary":"Name a climate sensor","value":{"name":"Bathroom"}},"contactName":{"summary":"Name a door sensor","value":{"name":"Warehouse door"}},"unicodeName":{"summary":"Unicode names","value":{"name":"Коридор"}},"clearName":{"summary":"Remove a name","value":{"name":null}},"on":{"value":{"relay":"on","expires_in":60}},"off":{"value":{"relay":"off","expires_in":30}},"climateTiming":{"summary":"Climate timing in seconds","value":{"settings":{"sleep_seconds":600}}},"contactTiming":{"summary":"Contact timing in seconds","value":{"settings":{"awake_after_change_seconds":15}}},"relayTiming":{"summary":"Relay timing in seconds","value":{"settings":{"poll_interval_seconds":30}}},"detailsAndTiming":{"summary":"Save details and Contact timing together","value":{"name":"Entrance","location":"Ground floor","description":"Main entrance","settings":{"awake_after_change_seconds":15}}}}}}},"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 metadata, mixed metadata/relay fields, relay value or expiry."},"429":{"description":"IP temporarily blocked; inspect Retry-After."},"200":{"description":"Device metadata or timing settings saved. Pending settings apply at the next compatible device exchange.","content":{"application/json":{"schema":{"type":"object","properties":{"mac":{"type":"string"},"settings":{"$ref":"#/components/schemas/DeviceSettings"},"name":{"type":"string","nullable":true},"location":{"type":"string","nullable":true},"description":{"type":"string","nullable":true}}},"example":{"mac":"78:1C:3C:21:4F:54","name":"Entrance door","location":"Office · Ground floor","description":"Main entrance sensor."}}}}}},"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```\n\n### Device details: name, location and description\nDevices of any type can have an optional `name`, stored on the server. MAC remains the unique identifier within your organization; duplicate names are allowed. No firmware update, device wake-up or relay command is involved.\n\nSend `POST /api/v1/devices/{mac}` with any combination of `name` (up to 80 characters), `location` (160) and `description` (2,000). All are optional, trimmed Unicode strings. Description supports line breaks and tabs; other control characters are rejected. Omitted fields remain unchanged. Set a field to null or an empty string to clear it. Send metadata separately from relay commands; mixing them returns 422 and changes nothing. Unknown devices, including those outside your token's organization, return 404. No firmware changes are needed.\n\nExamples: `{\"location\":\"Office · First floor\"}` changes only location; `{\"description\":\"Near the entrance.\\nInstalled September 2026.\"}` updates notes; `{\"location\":null,\"description\":null}` clears both while preserving the name. GET device/list responses include all three fields next to MAC, with null for unset values. History responses keep only the current name to avoid repeating long metadata.\n\n```bash\ncurl -X POST \"$BASE_URL/api/v1/devices/78:1C:3C:21:4F:54\" \\\n  -H \"X-API-Token: $API_TOKEN\" -H \"Content-Type: application/json\" \\\n  --data '{\"name\":\"Bathroom\"}'\n# Response (if other fields are unset): {\"mac\":\"78:1C:3C:21:4F:54\",\"name\":\"Bathroom\",\"location\":null,\"description\":null}\n```\n\nGET device/list responses include `name` immediately after `mac` (null when unset). GET measurements also returns the current `name` immediately after `device`, for raw readings, aggregated readings and command history, including empty results. The name is not repeated in each history row and is not a historical snapshot: renaming changes this top-level label even when querying old readings. New webhook payloads include `device.name`; `{{device.name}}` in destination URLs uses the name, falling back to MAC. Values are URL-encoded automatically. Already queued deliveries retain the name captured when queued. Renaming does not trigger webhooks. Test deliveries use an example name.\n\n### Device timing settings\nSend `POST /api/v1/devices/{mac}` containing `settings`, optionally together with `name`, `location` and `description`. The edit is atomic: an invalid field prevents all changes. The settings object must contain exactly the field supported by this device type:\n\n| Type | Field | Allowed seconds | Default in the current script |\n| --- | --- | --- | --- |\n| Climate | `sleep_seconds` | 180–21600 | 300 |\n| Contact | `awake_after_change_seconds` | 5–300 | 15 for DS100, 10 for RH-MAGv3B |\n| Relay | `poll_interval_seconds` | 5–3600 | 10 |\n\nExamples: `{\"settings\":{\"sleep_seconds\":600}}`, `{\"settings\":{\"awake_after_change_seconds\":15}}`, `{\"settings\":{\"poll_interval_seconds\":30}}`.\n\nValues must be JSON integers, not strings. Fields for another device type, empty settings, unknown fields and edits mixed with relay commands return 422 without changing anything. Devices outside your organization return 404. Omitted metadata or settings remain unchanged.\n\nFor example: `{\"name\":\"Entrance\",\"location\":\"Ground floor\",\"description\":\"Main entrance\",\"settings\":{\"awake_after_change_seconds\":15}}` saves all four fields in one request.\n\nThe short `z` parameter is only used by the device transport (`/i`) to save URL space. Public JSON uses descriptive names in `data`: `sleep_seconds` for Climate, `awake_after_change_seconds` for Contact, and `poll_interval_seconds` for Relay. These are the intervals reported with that reading, in seconds, not the latest requested settings. `settings.desired` and `settings.reported` describe the current configuration exchange. Historical readings retain their recorded interval; older stored `z` fields are presented using the same descriptive names.\n\nSettings are stored on the server and delivered on the device's next request. This does not wake a sleeping device. Updated BAT scripts are required: older scripts continue working but cannot apply timing settings. GET device and device-list responses include `settings.desired`, `settings.reported` and `settings.status`. `not_configured` means no remote override has been saved. `pending` means the latest report does not yet confirm the desired value. `applied` means the device has reported that value; merely sending a reply does not confirm application. `reported` is null until a compatible script reports its interval. Confirmation can arrive at the following normal report; no extra battery-powered report is forced.\n\nThe script stores the received setting as a startup value and uses it after reboot. Until a first successful configuration exchange it uses its local default. The relay still starts OFF after power-up. Relay timing controls only idle polling for remote commands: button/switch changes and command acknowledgements are sent promptly. Climate timing is deep-sleep duration; wake-up and connection time add to the actual interval between readings. Contact timing is its inactivity window, renewed after changes; queued/unconfirmed data may keep it awake longer. Its six-hour check-in and five-minute service-mode limit are unchanged. Increasing an interval increases remote-command latency or delays readings; shortening battery-device intervals increases battery consumption.\n\n```bash\ncurl -X POST \"$BASE_URL/api/v1/devices/78:1C:3C:21:4F:54\" \\\n  -H \"X-API-Token: $API_TOKEN\" -H \"Content-Type: application/json\" \\\n  --data '{\"settings\":{\"sleep_seconds\":600}}'\n# {\"mac\":\"78:1C:3C:21:4F:54\",\"settings\":{\"desired\":{\"sleep_seconds\":600},\"reported\":null,\"status\":\"pending\"}}\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":{"mac":"78:1C:3C:21:4F:54","name":"Bathroom","location":"Office","description":null,"organization":"Public Demo Organization","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":[]}},"status":{"availability":"online","issues":[]},"settings":{"desired":null,"reported":null,"status":"not_configured"}}},"relay":{"summary":"Relay — ON, command acknowledged","value":{"mac":"AA:BB:CC:DD:EE:03","name":"Corridor light","location":"Office","description":null,"organization":"Public Demo Organization","type":"relay","last_seen":"2026-09-13T10:00:10Z","status":{"availability":"online","issues":[]},"latest":{"source":"api","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"},"settings":{"desired":null,"reported":null,"status":"not_configured"}}},"contact":{"summary":"Contact — door/window closed and battery","value":{"mac":"B8:06:0D:78:48:4B","name":"Warehouse door","location":"Office","description":null,"organization":"Public Demo Organization","type":"contact","last_seen":"2026-09-13T10:00:00Z","status":{"availability":"online","issues":[]},"latest":{"data":{"contact_state":"closed","battery_voltage_v":2.85},"quality":{"status":"valid","issues":[]}},"settings":{"desired":null,"reported":null,"status":"not_configured"}}}}}}},"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","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","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","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","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, `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 (`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. Text fields such as `contact_state` expose only the last value in each interval (avg/min/max are null); this does not preserve every opening and closing. 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```\n\nResponses include the current device `name` immediately after `device`, or null when unnamed. This applies to raw/aggregated readings and type=commands; names are not historical snapshots.\n\nTiming readings use sleep_seconds, awake_after_change_seconds or poll_interval_seconds, never the transport abbreviation z. Non-numeric fields in aggregated intervals expose only last; avg/min/max are null. Use raw to retain every state transition. The dashboard draws categorical values as labelled step charts; numeric values use numeric scales.","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":"exclude_checkins","in":"query","required":false,"description":"Exclude periodic check-in reports, including from pagination totals. Use true for Contact history; leave false for battery charts. Check-ins refresh latest readings and last_seen without implying a door transition. If a check-in discovers a different contact position, a separate ordinary reading records it at detection time and ordinary webhooks are evaluated. Subscribe to the checkin event explicitly for periodic-check notifications.","schema":{"type":"boolean","default":false}},{"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","name":"Example device","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}}},"relay_sources":{"summary":"Relay — button ON, API OFF","value":{"device":"AA:BB:CC:DD:EE:03","name":"Example device","type":"relay","interval":"raw","from":"2026-09-21T00:00:00Z","to":"2026-09-21T23:59:59Z","count":2,"data":[{"time":"2026-09-21T10:00:00Z","source":"button","data":{"relay_state":"on"},"quality":{"status":"valid","issues":[]}},{"time":"2026-09-21T10:01:00Z","source":"api","data":{"relay_state":"off"},"quality":{"status":"valid","issues":[]}}],"initial_state":null,"last_seen":"2026-09-21T10:01:10Z","pagination":{"page":1,"limit":500,"total_records":2,"total_pages":1,"has_previous":false,"has_next":false}}},"contact_open":{"summary":"Contact history filtered by contact_state=open","value":{"device":"AA:BB:CC:DD:EE:02","name":"Example 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","name":"Example 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","name":"Example 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\nThe top-level `name` is the device's current name, or null when no name has been set. It is returned once beside `device`, not repeated in each event, including when the events list is empty.\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","checkin"],"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","name":"Office climate","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","name":null,"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","method":"POST","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","method":"POST","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 or availability events, instead of ordinary measurements. For `offline` / `online`, use empty conditions and `mode=every_match`: one notification per transition, offline detected by hourly cron after over 24 hours without contact, recovery detected immediately on ingestion. `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{{data.temperature_c}}%20C`. Keep placeholder braces literal, as `{{data.temperature_c}}`; spaces and other URL characters remain encoded. The API normalizes encoded placeholder braces when saving a URL.\n\n### Outgoing HTTP method\n`method` selects how notifications are delivered: `POST` (default) sends the standard event JSON body with `Content-Type: application/json`; `GET` sends no body, so include needed values in the destination URL using placeholders. Both require a public HTTPS destination and support URL placeholders, signatures, delivery history and retries. Creating or editing a webhook still uses POST to this API regardless of the outgoing method.\n\nExisting webhooks continue to use POST. Omitting method on update preserves it. `POST /webhooks/{id}/test` uses the saved method and synthetic example data. Delivery history `payload` is the event context; for GET it is not transmitted as a body. Pending deliveries and retries use the webhook's current URL and method at send time. Receivers must tolerate duplicate deliveries and use X-Webhook-Id for deduplication. Any 2xx response confirms success; redirects are not followed.\n\nPOST example configuration: `{\"url\":\"https://example.com/webhook\",\"method\":\"POST\",\"devices\":[\"78:1C:3C:21:4F:54\"],\"conditions\":{\"temperature_c\":{\"gt\":25}}}`.\n\nGET example configuration: `{\"url\":\"https://example.com/notify?device={{device.mac}}&temperature={{data.temperature_c}}\",\"method\":\"GET\",\"devices\":[\"78:1C:3C:21:4F:54\"],\"conditions\":{\"temperature_c\":{\"gt\":25}}}`.\n\nTo change only delivery method, send `{\"method\":\"POST\"}` to `POST /api/v1/webhooks/{id}`. POST sends the standard event payload unless a custom `body` object is configured. Custom headers are not supported. To switch a custom-body webhook to GET, send `{\"method\":\"GET\",\"body\":null}`.\n\n### Custom POST body\nUse `body` to send your own JSON object, for example `{\"temperature\":\"{{data.temperature_c}}\",\"message\":\"Device {{device.mac}}\"}`. It replaces the entire standard JSON body. The same placeholders as destination URLs work in JSON values, including `{{data.contact_state}}`, `{{data.relay_state}}`, `{{delta.temperature_c}}` and `{{previous.temperature_c}}`. Keys cannot contain placeholders. Nested objects and arrays are supported. Templates are limited to 16 KiB and 12 nesting levels; rendered bodies are limited to 64 KiB.\n\nA placeholder occupying the entire string preserves its JSON type: `\"{{data.temperature_c}}\"` becomes a number. Missing whole-value placeholders become null; missing placeholders inside text become empty strings. Embedded values become text. JSON escaping happens after substitution; never URL-encode body values. Substitution is single-pass. An empty object sends `{}`; `body:null` restores the standard event JSON. Omission on update preserves the template. GET cannot have a custom body.\n\nSignatures cover the exact rendered POST body. Delivery history `request_body` contains that JSON text for the latest recorded attempt, not the template, and remains unchanged when webhook settings are edited. It is null for GET, older history or when no body was captured. Retries use the current webhook configuration and replace this latest-attempt snapshot. The saved event payload itself is retained separately; custom bodies do not change condition evaluation.\n\nTelegram example: `{\"url\":\"https://api.telegram.org/botYOUR_BOT_TOKEN/sendMessage\",\"method\":\"POST\",\"body\":{\"chat_id\":\"YOUR_CHAT_ID\",\"text\":\"Door: {{data.contact_state}}\\nDevice: {{device.mac}}\"}}`. Use this with your device filter and conditions. A test uses synthetic example values and really sends a notification.\n\n### Device metadata placeholders\nUse `{{device.location}}` and `{{device.description}}` in destination URLs (GET or POST) and custom POST body values. They use device metadata, not measurement fields. Values are captured when the event is queued; later edits do not rewrite queued events. Supported for ordinary, delta and availability events. URL substitutions are URL-encoded automatically. Missing metadata becomes an empty string in URLs or embedded text, and null for whole-value JSON placeholders. Standard webhook JSON includes device.location and device.description; test notifications use example metadata. Example custom body: `{\"room\":\"{{device.location}}\",\"notes\":\"{{device.description}}\",\"message\":\"Door at {{device.location}}: {{data.contact_state}}\"}`.","security":[{"ApiToken":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/WebhookInput"},"examples":{"availability":{"summary":"Offline and recovery","description":"One notification per availability transition. Offline is checked hourly after over 24 hours of silence; recovery is queued on the next saved report. No measurement conditions.","value":{"name":"Offline and recovery","url":"https://example.com/notify?status={{event}}","devices":[],"events":["offline","online"],"mode":"every_match","conditions":{}}},"offline_only":{"summary":"Offline only","value":{"url":"https://example.com/notify?status={{event}}","devices":["78:1C:3C:21:4F:54"],"events":["offline"],"mode":"every_match","conditions":{}}},"online_only":{"summary":"Recovery only","value":{"url":"https://example.com/notify?status={{event}}","devices":["78:1C:3C:21:4F:54"],"events":["online"],"mode":"every_match","conditions":{}}},"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{{data.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{{data.temperature_c}}%20C%2C%20humidity:%20{{data.humidity_percent}}%25","devices":["78:1C:3C:21:4F:54"],"events":[],"match":"all","mode":"on_trigger","conditions":{"temperature_c":{"gt":28}}}},"postJson":{"summary":"POST standard event JSON","value":{"url":"https://example.com/webhook","method":"POST","devices":["78:1C:3C:21:4F:54"],"conditions":{"temperature_c":{"gt":25}}}},"getUrl":{"summary":"GET URL with placeholders, no body","value":{"url":"https://example.com/notify?device={{device.mac}}&temperature={{data.temperature_c}}","method":"GET","devices":["78:1C:3C:21:4F:54"],"conditions":{"temperature_c":{"gt":25}}}},"customBody":{"summary":"POST a custom JSON object","value":{"url":"https://example.com/webhook","method":"POST","body":{"device":"{{device.mac}}","temperature":"{{data.temperature_c}}","message":"Temperature changed"},"devices":["78:1C:3C:21:4F:54"],"conditions":{"temperature_c":{"delta":1}},"mode":"on_delta"}},"telegramBody":{"summary":"Telegram using POST JSON, not URL parameters","value":{"url":"https://api.telegram.org/botYOUR_BOT_TOKEN/sendMessage","method":"POST","body":{"chat_id":"YOUR_CHAT_ID","text":"Door: {{data.contact_state}}\nDevice: {{device.mac}}"},"devices":["C8:47:8C:42:88:48"],"conditions":{"contact_state":{"eq":"open"}},"mode":"on_change"}}}}}},"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","method":"POST","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","method":"POST","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.\n\n### Outgoing HTTP method\n`method` selects how notifications are delivered: `POST` (default) sends the standard event JSON body with `Content-Type: application/json`; `GET` sends no body, so include needed values in the destination URL using placeholders. Both require a public HTTPS destination and support URL placeholders, signatures, delivery history and retries. Creating or editing a webhook still uses POST to this API regardless of the outgoing method.\n\nExisting webhooks continue to use POST. Omitting method on update preserves it. `POST /webhooks/{id}/test` uses the saved method and synthetic example data. Delivery history `payload` is the event context; for GET it is not transmitted as a body. Pending deliveries and retries use the webhook's current URL and method at send time. Receivers must tolerate duplicate deliveries and use X-Webhook-Id for deduplication. Any 2xx response confirms success; redirects are not followed.\n\nPOST example configuration: `{\"url\":\"https://example.com/webhook\",\"method\":\"POST\",\"devices\":[\"78:1C:3C:21:4F:54\"],\"conditions\":{\"temperature_c\":{\"gt\":25}}}`.\n\nGET example configuration: `{\"url\":\"https://example.com/notify?device={{device.mac}}&temperature={{data.temperature_c}}\",\"method\":\"GET\",\"devices\":[\"78:1C:3C:21:4F:54\"],\"conditions\":{\"temperature_c\":{\"gt\":25}}}`.\n\nTo change only delivery method, send `{\"method\":\"POST\"}` to `POST /api/v1/webhooks/{id}`. POST sends the standard event payload unless a custom `body` object is configured. Custom headers are not supported. To switch a custom-body webhook to GET, send `{\"method\":\"GET\",\"body\":null}`.\n\n### Custom POST body\nUse `body` to send your own JSON object, for example `{\"temperature\":\"{{data.temperature_c}}\",\"message\":\"Device {{device.mac}}\"}`. It replaces the entire standard JSON body. The same placeholders as destination URLs work in JSON values, including `{{data.contact_state}}`, `{{data.relay_state}}`, `{{delta.temperature_c}}` and `{{previous.temperature_c}}`. Keys cannot contain placeholders. Nested objects and arrays are supported. Templates are limited to 16 KiB and 12 nesting levels; rendered bodies are limited to 64 KiB.\n\nA placeholder occupying the entire string preserves its JSON type: `\"{{data.temperature_c}}\"` becomes a number. Missing whole-value placeholders become null; missing placeholders inside text become empty strings. Embedded values become text. JSON escaping happens after substitution; never URL-encode body values. Substitution is single-pass. An empty object sends `{}`; `body:null` restores the standard event JSON. Omission on update preserves the template. GET cannot have a custom body.\n\nSignatures cover the exact rendered POST body. Delivery history `request_body` contains that JSON text for the latest recorded attempt, not the template, and remains unchanged when webhook settings are edited. It is null for GET, older history or when no body was captured. Retries use the current webhook configuration and replace this latest-attempt snapshot. The saved event payload itself is retained separately; custom bodies do not change condition evaluation.\n\nTelegram example: `{\"url\":\"https://api.telegram.org/botYOUR_BOT_TOKEN/sendMessage\",\"method\":\"POST\",\"body\":{\"chat_id\":\"YOUR_CHAT_ID\",\"text\":\"Door: {{data.contact_state}}\\nDevice: {{device.mac}}\"}}`. Use this with your device filter and conditions. A test uses synthetic example values and really sends a notification.\n\n### Device metadata placeholders\nUse `{{device.location}}` and `{{device.description}}` in destination URLs (GET or POST) and custom POST body values. They use device metadata, not measurement fields. Values are captured when the event is queued; later edits do not rewrite queued events. Supported for ordinary, delta and availability events. URL substitutions are URL-encoded automatically. Missing metadata becomes an empty string in URLs or embedded text, and null for whole-value JSON placeholders. Standard webhook JSON includes device.location and device.description; test notifications use example metadata. Example custom body: `{\"room\":\"{{device.location}}\",\"notes\":\"{{device.description}}\",\"message\":\"Door at {{device.location}}: {{data.contact_state}}\"}`.","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{{data.temperature_c}}%20C%2C%20humidity:%20{{data.humidity_percent}}%25"}},"events":{"summary":"Change event filters","value":{"events":["wakeup","sleep","checkin"]}},"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}}}},"methodOnly":{"summary":"Switch delivery to POST, preserve other settings","value":{"method":"POST"}},"resetBody":{"summary":"Restore the standard JSON body","value":{"body":null}}}}}},"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","method":"POST","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","method":"POST"}]}}}},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/TooManyAttempts"},"401":{"$ref":"#/components/responses/Unauthorized"},"422":{"$ref":"#/components/responses/ValidationError"}},"description":" Each delivery records method (GET/POST) for its latest attempt, independently of later webhook edits. Older records and unsent deliveries have null when no method was recorded. This is delivery history, not a separate row for each retry."}}},"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"},"name":{"type":"string","nullable":true,"maxLength":80,"description":"Current device name, or null. Not repeated in history rows."},"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":"4.0.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."}}},"availability_cron":{"type":"object","description":"Public scheduled offline-check health. No organization identifiers or secret keys.","properties":{"status":{"type":"string","enum":["ok","warning","error","unknown"]},"last_started_at":{"type":["string","null"],"format":"date-time"},"last_success_at":{"type":["string","null"],"format":"date-time"},"last_failed_at":{"type":["string","null"],"format":"date-time"},"next_expected_at":{"type":["string","null"],"format":"date-time","description":"Approximate next run, not the hosting schedule."}}},"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."},"sleep_seconds":{"type":"integer","description":"Climate: reported deep-sleep duration in seconds."},"awake_after_change_seconds":{"type":"integer","description":"Contact: reported inactivity window in seconds."},"poll_interval_seconds":{"type":"integer","description":"Relay: reported idle polling interval in seconds."}},"additionalProperties":true},"Measurement":{"type":"object","properties":{"source":{"type":"string","enum":["button","switch","api","boot","unknown"],"description":"Raw relay reading origin, alongside data. Old/unclassified relay readings are unknown. Omitted for other device types. Time is server receipt time."},"device":{"type":"string","example":"78:1C:3C:21:4F:54"},"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","checkin"],"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. For Contact devices, positive battery voltage below 1.5 V adds a suspicious low_battery issue recommending battery replacement. Exactly 1.5 V does not trigger this warning. Non-positive voltage remains invalid. This configured warning threshold is not a universal battery specification.","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."}}},"DeviceStatus":{"type":"object","required":["availability","issues"],"description":"Calculated at request time. For Climate, Contact (six-hour check-ins), and Relay, offline means no server contact for over 24 hours, not proof of a hardware failure. Online means recently seen, not a live connection. Relay uses its polling last_seen, not the last state change. Historical measurement quality is unchanged.","properties":{"availability":{"type":"string","enum":["online","offline","unknown"]},"issues":{"type":"array","items":{"type":"object","required":["code","message"],"properties":{"code":{"type":"string"},"message":{"type":"string"}},"additionalProperties":true}}},"example":{"availability":"offline","issues":[{"code":"device_offline","message":"No contact with the server for over 24 hours."}]}},"Device":{"type":"object","description":"A device and its most recently stored state. name is editable server-side metadata, not a firmware name.","properties":{"mac":{"type":"string"},"name":{"type":"string","nullable":true,"maxLength":80},"location":{"type":"string","nullable":true,"maxLength":160},"description":{"type":"string","nullable":true,"maxLength":2000},"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"},"type":{"type":"string"},"last_seen":{"type":"string","format":"date-time"},"latest":{"$ref":"#/components/schemas/Measurement"},"status":{"$ref":"#/components/schemas/DeviceStatus"},"settings":{"$ref":"#/components/schemas/DeviceSettings"}}},"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"},"name":{"type":"string","nullable":true,"maxLength":80,"description":"Current device name, or null. Not repeated in history rows."},"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":{"$ref":"#/components/schemas/Measurement"}},"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","type","count","updates","last_id","has_more"],"properties":{"device":{"type":"string","example":"78:1C:3C:21:4F:54"},"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."},"source":{"$ref":"#/components/schemas/Measurement/properties/source"},"time":{"type":"string","format":"date-time"},"event":{"type":"string","enum":["boot","reboot","watchdog","crash","wakeup","sleep","checkin"],"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","required":["device","name","type","events"],"properties":{"device":{"type":"string"},"name":{"type":["string","null"],"description":"Current device name; null if unset."},"type":{"type":["string","null"]},"events":{"type":"array","items":{"type":"object","properties":{"event":{"type":"string","enum":["boot","reboot","watchdog","crash","wakeup","sleep","checkin"]},"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."},"method":{"type":"string","enum":["GET","POST"],"default":"POST","description":"Outgoing HTTP method. POST sends standard event JSON; GET sends URL parameters only. Omission on update preserves the current value."},"body":{"type":"object","nullable":true,"additionalProperties":true,"description":"POST-only custom JSON object template. null uses standard event JSON. Omission on update preserves the current template. Nested arrays/objects supported; placeholders in values only. Maximum 16 KiB, nesting 12 levels."},"devices":{"type":"array","maxItems":100,"description":"Empty means all devices.","items":{"type":"string"}},"events":{"type":"array","default":[],"description":"Empty or omitted: ordinary measurements only. Non-empty: listed lifecycle or availability events. Offline/online require empty conditions and every_match; notify once per transition. On update, omission preserves the filter; [] restores ordinary measurements.","items":{"type":"string","enum":["boot","reboot","watchdog","crash","wakeup","sleep","checkin","offline","online"]}},"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"},"method":"POST","request_body":{"type":"string","nullable":true,"description":"Exact JSON text supplied to the HTTP client for the latest recorded POST attempt. null for GET, old history or attempts without a captured body. The signature is computed over these exact bytes."},"response_body":{"type":"string","nullable":true,"description":"Response text from the latest attempt, at most 8192 bytes. Invalid UTF-8 is replaced for safe display. Empty string means an empty HTTP response; null means no response or older history."},"response_time_ms":{"type":"integer","nullable":true,"description":"cURL total request duration in milliseconds, including connection and response transfer. null when no timed request was made or for older history."},"response_truncated":{"type":"boolean","nullable":true,"description":"True when response capture exceeded 8 KiB. null for older history or when no timed request was made. Truncation does not cause retries."},"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"}}}}},"DeviceSettings":{"type":"object","required":["desired","reported","status"],"properties":{"desired":{"type":"object","nullable":true,"description":"Requested type-specific timing setting, or null if not configured.","additionalProperties":{"type":"integer"}},"reported":{"type":"object","nullable":true,"description":"Last interval actually reported by the device, or null.","additionalProperties":{"type":"integer"}},"status":{"type":"string","enum":["not_configured","pending","applied"]}}}},"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."}}}}}}}}}}