{
  "info": {
    "_postman_id": "6d80c420-82fd-4cbf-89c1-c771af54f4cc",
    "name": "DIENAS API — Public Demo",
    "description": "Distributed IoT Events, Notifications & Automation Service. Ready-to-run requests for the public climate sensor. The device sends temperature and humidity approximately every five minutes. All timestamps are ISO 8601 UTC; the trailing Z means UTC. The exact-range example automatically recalculates the start of today and tomorrow before every request. Authentication protection: more than 15 failed token attempts from the same IP within 15 minutes trigger a 15-minute IP block. HTTP 429 includes Retry-After in seconds; wait before retrying. Valid tokens do not bypass an active block.\n\nThree kinds of history:\n- Readings: GET /api/v1/measurements?device=MAC returns reported values in data. Repeated states are normal.\n- Device events: GET /api/v1/events?device=MAC returns lifecycle events in events (boot, reboot, wakeup, sleep), not door positions.\n- Relay commands: GET /api/v1/measurements?device=MAC&type=commands returns requested actions and execution status in commands. A queued ON command is not a confirmed ON state; check GET /api/v1/devices/{mac} for the latest reported state.",
    "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json"
  },
  "auth": {
    "type": "apikey",
    "apikey": [
      {
        "key": "key",
        "value": "X-API-Token",
        "type": "string"
      },
      {
        "key": "value",
        "value": "{{api_token}}",
        "type": "string"
      },
      {
        "key": "in",
        "value": "header",
        "type": "string"
      }
    ]
  },
  "variable": [
    {
      "key": "private_relay_token",
      "value": "",
      "type": "string"
    },
    {
      "key": "relay_mac",
      "value": "AA:BB:CC:DD:EE:03",
      "type": "string"
    },
    {
      "key": "base_url",
      "value": "https://dienas.lv",
      "type": "string"
    },
    {
      "key": "api_token",
      "value": "595a321cd5eedc5f6dd461d60a574719e142b5e53bee1a0e",
      "type": "string"
    },
    {
      "key": "device_mac",
      "value": "78:1C:3C:21:4F:54",
      "type": "string"
    },
    {
      "key": "last_update_id",
      "value": "0",
      "type": "string"
    },
    {
      "key": "webhook_url",
      "value": "https://example.com/webhook",
      "type": "string"
    },
    {
      "key": "telegram_bot_token",
      "value": "YOUR_BOT_TOKEN",
      "type": "string"
    },
    {
      "key": "telegram_chat_id",
      "value": "YOUR_CHAT_ID",
      "type": "string"
    },
    {
      "key": "webhook_id",
      "value": "",
      "type": "string"
    },
    {
      "key": "webhook_secret",
      "value": "",
      "type": "string"
    },
    {
      "key": "today_start",
      "value": "2026-09-01T00:00:00.000Z",
      "type": "string"
    },
    {
      "key": "tomorrow_start",
      "value": "2026-09-02T00:00:00.000Z",
      "type": "string"
    }
  ],
  "event": [
    {
      "listen": "prerequest",
      "script": {
        "type": "text/javascript",
        "exec": [
          "const today = new Date();",
          "today.setUTCHours(0, 0, 0, 0);",
          "const tomorrow = new Date(today);",
          "tomorrow.setUTCDate(tomorrow.getUTCDate() + 1);",
          "pm.collectionVariables.set('today_start', today.toISOString());",
          "pm.collectionVariables.set('tomorrow_start', tomorrow.toISOString());"
        ]
      }
    },
    {
      "listen": "test",
      "script": {
        "type": "text/javascript",
        "exec": [
          "pm.test('HTTP status is successful', function () {",
          "    pm.expect(pm.response.code).to.be.within(200, 299);",
          "});",
          "pm.test('Response is JSON', function () {",
          "    pm.expect(function () { pm.response.json(); }).not.to.throw();",
          "});"
        ]
      }
    }
  ],
  "item": [
    {
      "name": "Relay control (private token required)",
      "description": "Set private_relay_token and relay_mac before use. Never connect real relays to the public demo organization. One outstanding command per device; 202 is queued, not applied. GET shows latest command, not history. Polling is approximately 10 seconds after each successful response; execution is confirmed in the following report. Expired commands stop being delivered but expiry does not switch the load off. Boot interrupts pending commands. Local controls remain independent. After an ambiguous POST failure, inspect GET before retrying. Public responses use relay_state and reported_state as \"on\"/\"off\", never 0/1. Use interval=raw for state history at all ranges; aggregation omits string states.",
      "auth": {
        "type": "apikey",
        "apikey": [
          {
            "key": "key",
            "value": "X-API-Token",
            "type": "string"
          },
          {
            "key": "value",
            "value": "{{private_relay_token}}",
            "type": "string"
          },
          {
            "key": "in",
            "value": "header",
            "type": "string"
          }
        ]
      },
      "item": [
        {
          "name": "Turn ON (expires in 60 seconds)",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": "{{base_url}}/api/v1/devices/{{relay_mac}}",
            "body": {
              "mode": "raw",
              "raw": "{\"relay\":\"on\",\"expires_in\":60}"
            }
          }
        },
        {
          "name": "Get command acknowledgement and actual reported state",
          "request": {
            "method": "GET",
            "url": "{{base_url}}/api/v1/devices/{{relay_mac}}"
          }
        },
        {
          "name": "Command history — first page",
          "request": {
            "method": "GET",
            "url": "{{base_url}}/api/v1/measurements?device={{relay_mac}}&type=commands&period=7d&page=1&limit=20",
            "description": "Newest first. Each item shows the command's latest status, not individual delivery attempts. Follow pagination.has_next."
          }
        },
        {
          "name": "Command history — second page",
          "request": {
            "method": "GET",
            "url": "{{base_url}}/api/v1/measurements?device={{relay_mac}}&type=commands&period=7d&page=2&limit=20"
          }
        },
        {
          "name": "Turn OFF (after previous command finishes)",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": "{{base_url}}/api/v1/devices/{{relay_mac}}",
            "body": {
              "mode": "raw",
              "raw": "{\"relay\":\"off\",\"expires_in\":30}"
            }
          }
        },
        {
          "name": "Relay raw history for seven days",
          "request": {
            "description": "Relay history stores first state, state changes and lifecycle events, not unchanged polls. last_seen still updates every poll. initial_state is the last stored reading before from (or null), outside data/count/pagination. Draw this state from from, then transitions; extend only to min(to,last_seen). This represents last-known state, not continuous connectivity. Historic IDs may have gaps after duplicate cleanup.",
            "method": "GET",
            "url": "{{base_url}}/api/v1/measurements?device={{relay_mac}}&period=7d&interval=raw&page=1&limit=500"
          }
        },
        {
          "name": "Webhook — relay ON and OFF",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": "{{base_url}}/api/v1/webhooks",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"name\": \"Relay ON and OFF\",\n  \"url\": \"{{webhook_url}}\",\n  \"devices\": [\n    \"{{relay_mac}}\"\n  ],\n  \"mode\": \"on_change\",\n  \"conditions\": {\n    \"relay_state\": {\n      \"eq\": \"on\"\n    }\n  }\n}"
            },
            "description": "Triggers on ON and recovers on OFF. Repeated states do not notify. Templates use {{data.relay_state}}. Use on_trigger for ON only or eq off for OFF only."
          }
        }
      ]
    },
    {
      "name": "Service",
      "item": [
        {
          "name": "Public health check",
          "auth": {
            "type": "noauth"
          },
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{base_url}}/health",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "health"
              ]
            },
            "description": "Public health check for the API and a live MySQL query. Because this request intentionally sends no token, webhook_queue is omitted."
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "const health = pm.response.json();",
                  "pm.test('Public health response hides queue information', function () {",
                  "    pm.expect(health.version).to.eql('3.4.0');",
                  "    pm.expect(health.checks.api.status).to.eql('ok');",
                  "    pm.expect(health.checks.database.response_time_ms).to.be.a('number');",
                  "    pm.expect(health.checks).not.to.have.property('webhook_queue');",
                  "});"
                ]
              }
            }
          ]
        },
        {
          "name": "Authenticated health check",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{base_url}}/health",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "health"
              ]
            },
            "description": "Uses the collection X-API-Token. webhook_queue contains only deliveries belonging to that token's organization. pending includes pending and retrying deliveries; failed counts terminal failures."
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "const health = pm.response.json();",
                  "pm.test('Authenticated health contains scoped queue information', function () {",
                  "    pm.expect(health.version).to.eql('3.4.0');",
                  "    pm.expect(health.checks.webhook_queue).to.have.all.keys('status', 'pending', 'failed');",
                  "    pm.expect(health.checks.webhook_queue.pending).to.be.a('number');",
                  "    pm.expect(health.checks.webhook_queue.failed).to.be.a('number');",
                  "});"
                ]
              }
            }
          ]
        }
      ]
    },
    {
      "name": "Devices",
      "item": [
        {
          "name": "List devices",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{base_url}}/api/v1/devices",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "api",
                "v1",
                "devices"
              ]
            },
            "description": "Lists all devices available through the public demo token."
          }
        },
        {
          "name": "Get demo device",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{base_url}}/api/v1/devices/{{device_mac}}",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "api",
                "v1",
                "devices",
                "{{device_mac}}"
              ]
            },
            "description": "Returns the public climate sensor and its latest state."
          }
        },
        {
          "name": "Updates — initial sync and save last_id",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{base_url}}/api/v1/devices/{{device_mac}}/updates",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "api",
                "v1",
                "devices",
                "{{device_mac}}",
                "updates"
              ]
            },
            "description": "First polling request. With no after_id, the API returns only the newest record. The test script saves response.last_id into the collection variable last_update_id."
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "const result = pm.response.json();",
                  "pm.collectionVariables.set('last_update_id', String(result.last_id));",
                  "pm.test('Current record and cursor returned', function () {",
                  "    pm.expect(result.count).to.eql(1);",
                  "    pm.expect(result.updates).to.have.lengthOf(1);",
                  "    pm.expect(result.last_id).to.be.a('number');",
                  "});"
                ]
              }
            }
          ]
        },
        {
          "name": "Updates — poll after saved last_id",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{base_url}}/api/v1/devices/{{device_mac}}/updates?after_id={{last_update_id}}&limit=100",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "api",
                "v1",
                "devices",
                "{{device_mac}}",
                "updates"
              ],
              "query": [
                {
                  "key": "after_id",
                  "value": "{{last_update_id}}"
                },
                {
                  "key": "limit",
                  "value": "100"
                }
              ]
            },
            "description": "Normal polling request. An empty updates array means no new records. Save the returned last_id after processing the batch; if has_more is true, request the next batch immediately."
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "const result = pm.response.json();",
                  "pm.collectionVariables.set('last_update_id', String(result.last_id));"
                ]
              }
            }
          ]
        },
        {
          "name": "Updates — small backlog batch",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{base_url}}/api/v1/devices/{{device_mac}}/updates?after_id=0&limit=3",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "api",
                "v1",
                "devices",
                "{{device_mac}}",
                "updates"
              ],
              "query": [
                {
                  "key": "after_id",
                  "value": "0"
                },
                {
                  "key": "limit",
                  "value": "3"
                }
              ]
            },
            "description": "Demonstrates batching. Starting at zero returns the oldest records for this device. With more records waiting, has_more is true; use the returned last_id as the next after_id. IDs start at 1 and increase by one independently for each organization and device."
          }
        }
      ]
    },
    {
      "name": "Measurements",
      "item": [
        {
          "name": "Raw data — first page (limit 10)",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{base_url}}/api/v1/measurements?device={{device_mac}}&period=24h&interval=raw&page=1&limit=10",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "api",
                "v1",
                "measurements"
              ],
              "query": [
                {
                  "key": "device",
                  "value": "{{device_mac}}"
                },
                {
                  "key": "period",
                  "value": "24h"
                },
                {
                  "key": "interval",
                  "value": "raw"
                },
                {
                  "key": "page",
                  "value": "1"
                },
                {
                  "key": "limit",
                  "value": "10"
                }
              ]
            },
            "description": "Returns the first ten raw samples and page information."
          }
        },
        {
          "name": "Raw data — second page (limit 10)",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{base_url}}/api/v1/measurements?device={{device_mac}}&period=24h&interval=raw&page=2&limit=10",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "api",
                "v1",
                "measurements"
              ],
              "query": [
                {
                  "key": "device",
                  "value": "{{device_mac}}"
                },
                {
                  "key": "period",
                  "value": "24h"
                },
                {
                  "key": "interval",
                  "value": "raw"
                },
                {
                  "key": "page",
                  "value": "2"
                },
                {
                  "key": "limit",
                  "value": "10"
                }
              ]
            },
            "description": "Returns the second page of raw samples using the same range and page size."
          }
        },
        {
          "name": "Last five days — automatic aggregation",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{base_url}}/api/v1/measurements?device={{device_mac}}&period=5d",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "api",
                "v1",
                "measurements"
              ],
              "query": [
                {
                  "key": "device",
                  "value": "{{device_mac}}"
                },
                {
                  "key": "period",
                  "value": "5d"
                }
              ]
            },
            "description": "Returns chart-friendly measurements for the last five days. Aggregated points preserve calculated values and report quality.status, flagged_samples, and issue occurrence counts."
          }
        },
        {
          "name": "Today to tomorrow — exact UTC range",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{base_url}}/api/v1/measurements?device={{device_mac}}&from={{today_start}}&to={{tomorrow_start}}&interval=1h",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "api",
                "v1",
                "measurements"
              ],
              "query": [
                {
                  "key": "device",
                  "value": "{{device_mac}}"
                },
                {
                  "key": "from",
                  "value": "{{today_start}}",
                  "description": "Calculated automatically as 00:00:00 UTC today."
                },
                {
                  "key": "to",
                  "value": "{{tomorrow_start}}",
                  "description": "Calculated automatically as 00:00:00 UTC tomorrow."
                },
                {
                  "key": "interval",
                  "value": "1h"
                }
              ]
            },
            "description": "Demonstrates an exact from/to range. The collection pre-request script updates both dates automatically; results are grouped by hour."
          }
        },
        {
          "name": "Last three calendar months — grouped by day",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{base_url}}/api/v1/measurements?device={{device_mac}}&period=3m&interval=1d",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "api",
                "v1",
                "measurements"
              ],
              "query": [
                {
                  "key": "device",
                  "value": "{{device_mac}}"
                },
                {
                  "key": "period",
                  "value": "3m"
                },
                {
                  "key": "interval",
                  "value": "1d"
                }
              ]
            },
            "description": "Demonstrates a longer relative period and daily aggregation. In period values, m means calendar months."
          }
        },
        {
          "name": "Contact history — both positions, last 30 days",
          "request": {
            "method": "GET",
            "header": [],
            "url": "{{base_url}}/api/v1/measurements?device=B8:06:0D:78:48:4B&period=30d&interval=raw&page=1&limit=500",
            "description": "Contact and Relay default to interval=raw at ANY time range when interval is omitted. This returns both open and closed contact_state values for a step chart. Aggregated intervals (15m, 1h, 6h, 1d) calculate numeric statistics and omit text contact states; only Climate automatically selects aggregation for longer ranges. Explicit intervals still take precedence. Battery voltage can still be aggregated, but opening/closing history requires raw. Follow pagination.has_next and increment page for all records; use fixed from/to dates for a stable export. Repeated positions are possible. Replace the demo MAC with your device."
          }
        },
        {
          "name": "Contact history — open positions",
          "request": {
            "method": "GET",
            "header": [],
            "url": "{{base_url}}/api/v1/measurements?device=AA:BB:CC:DD:EE:02&period=7d&interval=raw&contact_state=open&page=1&limit=100",
            "description": "Replace the example MAC with your contact sensor. Returns records whose data.contact_state is open, without an event property on ordinary samples. Set contact_state=closed for closed positions or omit the filter for both. Repeated states may be reported. Pagination counts only matching rows."
          }
        }
      ]
    },
    {
      "name": "Events",
      "item": [
        {
          "name": "All device events",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{base_url}}/api/v1/events?device={{device_mac}}&limit=100",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "api",
                "v1",
                "events"
              ],
              "query": [
                {
                  "key": "device",
                  "value": "{{device_mac}}"
                },
                {
                  "key": "limit",
                  "value": "100"
                }
              ]
            },
            "description": "Returns only device lifecycle events. Ordinary measurements and contact positions are available through /measurements."
          }
        },
        {
          "name": "Wakeup events",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{base_url}}/api/v1/events?device={{device_mac}}&type=wakeup&limit=100",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "api",
                "v1",
                "events"
              ],
              "query": [
                {
                  "key": "device",
                  "value": "{{device_mac}}"
                },
                {
                  "key": "type",
                  "value": "wakeup"
                },
                {
                  "key": "limit",
                  "value": "100"
                }
              ]
            },
            "description": "Returns moments when the device entered service mode."
          }
        },
        {
          "name": "Sleep events",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{base_url}}/api/v1/events?device={{device_mac}}&type=sleep&limit=100",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "api",
                "v1",
                "events"
              ],
              "query": [
                {
                  "key": "device",
                  "value": "{{device_mac}}"
                },
                {
                  "key": "type",
                  "value": "sleep"
                },
                {
                  "key": "limit",
                  "value": "100"
                }
              ]
            },
            "description": "Returns planned transitions into deep sleep."
          }
        },
        {
          "name": "Power-on events",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{base_url}}/api/v1/events?device={{device_mac}}&type=boot&limit=100",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "api",
                "v1",
                "events"
              ],
              "query": [
                {
                  "key": "device",
                  "value": "{{device_mac}}"
                },
                {
                  "key": "type",
                  "value": "boot"
                },
                {
                  "key": "limit",
                  "value": "100"
                }
              ]
            },
            "description": "Returns cold power-on events. When supplied, data.reboot_reason is 0."
          }
        },
        {
          "name": "Software reboot events",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{base_url}}/api/v1/events?device={{device_mac}}&type=reboot&limit=100",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "api",
                "v1",
                "events"
              ],
              "query": [
                {
                  "key": "device",
                  "value": "{{device_mac}}"
                },
                {
                  "key": "type",
                  "value": "reboot"
                },
                {
                  "key": "limit",
                  "value": "100"
                }
              ]
            },
            "description": "Returns software restart events. Watchdog and crash restarts can be queried with type=watchdog and type=crash."
          }
        }
      ]
    },
    {
      "name": "Webhooks",
      "description": "Create, inspect, update, test and delete webhooks. Creation stores webhook_id and the one-time webhook_secret as collection variables. The secret does not decrypt the JSON body: receivers verify X-Webhook-Signature by calculating sha256=HMAC-SHA256(secret, X-Webhook-Timestamp + '.' + exact_raw_request_body). Use the raw body before JSON parsing, compare in constant time, and reject timestamps older than five minutes. Telegram ignores these custom signature headers; verification is intended for receiving servers you control.",
      "item": [
        {
          "name": "Create delta webhook — temperature, humidity and battery",
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "if (pm.response.code === 201) { const body = pm.response.json(); pm.collectionVariables.set('webhook_id', body.id); pm.collectionVariables.set('webhook_secret', body.secret); }"
                ]
              }
            }
          ],
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": "{{base_url}}/api/v1/webhooks",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"name\": \"Metric changes\",\n  \"url\": \"{{webhook_url}}\",\n  \"devices\": [\n    \"{{device_mac}}\"\n  ],\n  \"events\": [],\n  \"mode\": \"on_delta\",\n  \"match\": \"any\",\n  \"conditions\": {\n    \"temperature_c\": {\n      \"delta\": 1\n    },\n    \"humidity_percent\": {\n      \"delta\": 5\n    },\n    \"battery_voltage_v\": {\n      \"delta\": 0.1\n    }\n  }\n}"
            },
            "description": "First numeric sample silently initializes each device/metric baseline. Any change of at least ±1 °C, ±5 humidity percentage points or ±0.1 V queues measurement.changed. Only triggering metrics reset their baselines. match=all requires every metric to reach its threshold in the same sample. Missing/non-numeric values do not reset baselines. Deliveries include changes with previous, current, signed delta and threshold. Configuration changes reset baselines; pause/resume preserves them."
          }
        },
        {
          "name": "Update webhook — Telegram on temperature change ±1 °C",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": "{{base_url}}/api/v1/webhooks/{{webhook_id}}",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"url\": \"https://api.telegram.org/bot{{telegram_bot_token}}/sendMessage?chat_id={{telegram_chat_id}}&text=Temperature:%20{{temperature_c}}%20C%2C%20change:%20{{delta.temperature_c}}%20C\",\n  \"mode\": \"on_delta\",\n  \"match\": \"any\",\n  \"events\": [],\n  \"conditions\": {\n    \"temperature_c\": {\n      \"delta\": 1\n    }\n  }\n}"
            },
            "description": "Set telegram_bot_token, telegram_chat_id and webhook_id first. Baseline example: 24 → 24.4 → 24.8 → 25.1 sends +1.1; then 24.0 sends -1.1. Literal sensor placeholders are expanded by the API. Do not define Postman variables named temperature_c or delta.temperature_c. Editing the conditions starts a new silent baseline."
          }
        },
        {
          "name": "More webhook recipes",
          "description": "Illustrative examples. Replace webhook_url and non-demo MAC addresses before sending. Running a request creates an enabled webhook; saving an example does not. Thresholds are examples, not device safety ratings.",
          "item": [
            {
              "name": "Low battery and recovery",
              "event": [
                {
                  "listen": "test",
                  "script": {
                    "type": "text/javascript",
                    "exec": [
                      "if (pm.response.code === 201) { const w = pm.response.json(); pm.collectionVariables.set('webhook_id', w.id); pm.collectionVariables.set('webhook_secret', w.secret); }"
                    ]
                  }
                }
              ],
              "request": {
                "method": "POST",
                "header": [
                  {
                    "key": "Content-Type",
                    "value": "application/json"
                  }
                ],
                "url": "{{base_url}}/api/v1/webhooks",
                "body": {
                  "mode": "raw",
                  "raw": "{\n  \"url\": \"{{webhook_url}}\",\n  \"devices\": [\n    \"{{device_mac}}\"\n  ],\n  \"events\": [],\n  \"match\": \"any\",\n  \"name\": \"Low battery and recovery\",\n  \"mode\": \"on_change\",\n  \"conditions\": {\n    \"battery_voltage_v\": {\n      \"lt\": 2.2\n    }\n  }\n}"
                },
                "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."
              }
            },
            {
              "name": "Temperature AND humidity",
              "event": [
                {
                  "listen": "test",
                  "script": {
                    "type": "text/javascript",
                    "exec": [
                      "if (pm.response.code === 201) { const w = pm.response.json(); pm.collectionVariables.set('webhook_id', w.id); pm.collectionVariables.set('webhook_secret', w.secret); }"
                    ]
                  }
                }
              ],
              "request": {
                "method": "POST",
                "header": [
                  {
                    "key": "Content-Type",
                    "value": "application/json"
                  }
                ],
                "url": "{{base_url}}/api/v1/webhooks",
                "body": {
                  "mode": "raw",
                  "raw": "{\n  \"url\": \"{{webhook_url}}\",\n  \"devices\": [\n    \"{{device_mac}}\"\n  ],\n  \"events\": [],\n  \"match\": \"all\",\n  \"name\": \"Temperature AND humidity\",\n  \"mode\": \"on_change\",\n  \"conditions\": {\n    \"temperature_c\": {\n      \"gte\": 28\n    },\n    \"humidity_percent\": {\n      \"gte\": 75\n    }\n  }\n}"
                },
                "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."
              }
            },
            {
              "name": "Humidity above 75%",
              "event": [
                {
                  "listen": "test",
                  "script": {
                    "type": "text/javascript",
                    "exec": [
                      "if (pm.response.code === 201) { const w = pm.response.json(); pm.collectionVariables.set('webhook_id', w.id); pm.collectionVariables.set('webhook_secret', w.secret); }"
                    ]
                  }
                }
              ],
              "request": {
                "method": "POST",
                "header": [
                  {
                    "key": "Content-Type",
                    "value": "application/json"
                  }
                ],
                "url": "{{base_url}}/api/v1/webhooks",
                "body": {
                  "mode": "raw",
                  "raw": "{\n  \"url\": \"{{webhook_url}}\",\n  \"devices\": [\n    \"{{device_mac}}\"\n  ],\n  \"events\": [],\n  \"match\": \"any\",\n  \"name\": \"Humidity above 75%\",\n  \"mode\": \"on_change\",\n  \"conditions\": {\n    \"humidity_percent\": {\n      \"gt\": 75\n    }\n  }\n}"
                },
                "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."
              }
            },
            {
              "name": "Humidity below 40%",
              "event": [
                {
                  "listen": "test",
                  "script": {
                    "type": "text/javascript",
                    "exec": [
                      "if (pm.response.code === 201) { const w = pm.response.json(); pm.collectionVariables.set('webhook_id', w.id); pm.collectionVariables.set('webhook_secret', w.secret); }"
                    ]
                  }
                }
              ],
              "request": {
                "method": "POST",
                "header": [
                  {
                    "key": "Content-Type",
                    "value": "application/json"
                  }
                ],
                "url": "{{base_url}}/api/v1/webhooks",
                "body": {
                  "mode": "raw",
                  "raw": "{\n  \"url\": \"{{webhook_url}}\",\n  \"devices\": [\n    \"{{device_mac}}\"\n  ],\n  \"events\": [],\n  \"match\": \"any\",\n  \"name\": \"Humidity below 40%\",\n  \"mode\": \"on_change\",\n  \"conditions\": {\n    \"humidity_percent\": {\n      \"lt\": 40\n    }\n  }\n}"
                },
                "description": "Trigger below 40%, recovery at 40% or above. Use alongside the high-humidity example to monitor values outside 40–75%."
              }
            },
            {
              "name": "Door or gate opens and closes",
              "event": [
                {
                  "listen": "test",
                  "script": {
                    "type": "text/javascript",
                    "exec": [
                      "if (pm.response.code === 201) { const w = pm.response.json(); pm.collectionVariables.set('webhook_id', w.id); pm.collectionVariables.set('webhook_secret', w.secret); }"
                    ]
                  }
                }
              ],
              "request": {
                "method": "POST",
                "header": [
                  {
                    "key": "Content-Type",
                    "value": "application/json"
                  }
                ],
                "url": "{{base_url}}/api/v1/webhooks",
                "body": {
                  "mode": "raw",
                  "raw": "{\n  \"url\": \"{{webhook_url}}\",\n  \"devices\": [\n    \"AA:BB:CC:DD:EE:02\"\n  ],\n  \"events\": [],\n  \"match\": \"any\",\n  \"name\": \"Door or gate opens and closes\",\n  \"mode\": \"on_change\",\n  \"conditions\": {\n    \"contact_state\": {\n      \"eq\": \"open\"\n    }\n  }\n}"
                },
                "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."
              }
            },
            {
              "name": "Device restarts and crashes",
              "event": [
                {
                  "listen": "test",
                  "script": {
                    "type": "text/javascript",
                    "exec": [
                      "if (pm.response.code === 201) { const w = pm.response.json(); pm.collectionVariables.set('webhook_id', w.id); pm.collectionVariables.set('webhook_secret', w.secret); }"
                    ]
                  }
                }
              ],
              "request": {
                "method": "POST",
                "header": [
                  {
                    "key": "Content-Type",
                    "value": "application/json"
                  }
                ],
                "url": "{{base_url}}/api/v1/webhooks",
                "body": {
                  "mode": "raw",
                  "raw": "{\n  \"url\": \"{{webhook_url}}\",\n  \"devices\": [\n    \"{{device_mac}}\"\n  ],\n  \"events\": [\n    \"reboot\",\n    \"watchdog\",\n    \"crash\"\n  ],\n  \"match\": \"any\",\n  \"name\": \"Device restarts and crashes\",\n  \"mode\": \"every_match\",\n  \"conditions\": {}\n}"
                },
                "description": "Forward every reported reboot, watchdog or crash event. This cannot detect sudden power loss or silence; it only reacts to received events."
              }
            },
            {
              "name": "Forward all organization readings",
              "event": [
                {
                  "listen": "test",
                  "script": {
                    "type": "text/javascript",
                    "exec": [
                      "if (pm.response.code === 201) { const w = pm.response.json(); pm.collectionVariables.set('webhook_id', w.id); pm.collectionVariables.set('webhook_secret', w.secret); }"
                    ]
                  }
                }
              ],
              "request": {
                "method": "POST",
                "header": [
                  {
                    "key": "Content-Type",
                    "value": "application/json"
                  }
                ],
                "url": "{{base_url}}/api/v1/webhooks",
                "body": {
                  "mode": "raw",
                  "raw": "{\n  \"url\": \"{{webhook_url}}\",\n  \"devices\": [],\n  \"events\": [],\n  \"match\": \"any\",\n  \"name\": \"Forward all organization readings\",\n  \"mode\": \"every_match\",\n  \"conditions\": {}\n}"
                },
                "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."
              }
            },
            {
              "name": "Battery changes by ±0.1 V",
              "event": [
                {
                  "listen": "test",
                  "script": {
                    "type": "text/javascript",
                    "exec": [
                      "if (pm.response.code === 201) { const w = pm.response.json(); pm.collectionVariables.set('webhook_id', w.id); pm.collectionVariables.set('webhook_secret', w.secret); }"
                    ]
                  }
                }
              ],
              "request": {
                "method": "POST",
                "header": [
                  {
                    "key": "Content-Type",
                    "value": "application/json"
                  }
                ],
                "url": "{{base_url}}/api/v1/webhooks",
                "body": {
                  "mode": "raw",
                  "raw": "{\n  \"url\": \"{{webhook_url}}\",\n  \"devices\": [\n    \"{{device_mac}}\"\n  ],\n  \"events\": [],\n  \"match\": \"any\",\n  \"name\": \"Battery changes by ±0.1 V\",\n  \"mode\": \"on_delta\",\n  \"conditions\": {\n    \"battery_voltage_v\": {\n      \"delta\": 0.1\n    }\n  }\n}"
                },
                "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."
              }
            },
            {
              "name": "Both temperature AND humidity change",
              "event": [
                {
                  "listen": "test",
                  "script": {
                    "type": "text/javascript",
                    "exec": [
                      "if (pm.response.code === 201) { const w = pm.response.json(); pm.collectionVariables.set('webhook_id', w.id); pm.collectionVariables.set('webhook_secret', w.secret); }"
                    ]
                  }
                }
              ],
              "request": {
                "method": "POST",
                "header": [
                  {
                    "key": "Content-Type",
                    "value": "application/json"
                  }
                ],
                "url": "{{base_url}}/api/v1/webhooks",
                "body": {
                  "mode": "raw",
                  "raw": "{\n  \"url\": \"{{webhook_url}}\",\n  \"devices\": [\n    \"{{device_mac}}\"\n  ],\n  \"events\": [],\n  \"match\": \"all\",\n  \"name\": \"Both temperature AND humidity change\",\n  \"mode\": \"on_delta\",\n  \"conditions\": {\n    \"temperature_c\": {\n      \"delta\": 1\n    },\n    \"humidity_percent\": {\n      \"delta\": 5\n    }\n  }\n}"
                },
                "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."
              }
            },
            {
              "name": "Custom numeric metric: pressure",
              "event": [
                {
                  "listen": "test",
                  "script": {
                    "type": "text/javascript",
                    "exec": [
                      "if (pm.response.code === 201) { const w = pm.response.json(); pm.collectionVariables.set('webhook_id', w.id); pm.collectionVariables.set('webhook_secret', w.secret); }"
                    ]
                  }
                }
              ],
              "request": {
                "method": "POST",
                "header": [
                  {
                    "key": "Content-Type",
                    "value": "application/json"
                  }
                ],
                "url": "{{base_url}}/api/v1/webhooks",
                "body": {
                  "mode": "raw",
                  "raw": "{\n  \"url\": \"{{webhook_url}}\",\n  \"devices\": [\n    \"AA:BB:CC:DD:EE:05\"\n  ],\n  \"events\": [],\n  \"match\": \"any\",\n  \"name\": \"Custom numeric metric: pressure\",\n  \"mode\": \"on_trigger\",\n  \"conditions\": {\n    \"pressure_kpa\": {\n      \"gt\": 120\n    }\n  }\n}"
                },
                "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."
              }
            }
          ]
        },
        {
          "name": "Create webhook",
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "if (pm.response.code === 201) {",
                  "    const body = pm.response.json();",
                  "    pm.collectionVariables.set('webhook_id', body.id);",
                  "    pm.collectionVariables.set('webhook_secret', body.secret);",
                  "}"
                ]
              }
            }
          ],
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "body": {
              "mode": "raw",
              "raw": "{\n  \"name\": \"High climate values\",\n  \"url\": \"{{webhook_url}}\",\n  \"devices\": [\n    \"{{device_mac}}\"\n  ],\n  \"events\": [],\n  \"match\": \"any\",\n  \"mode\": \"on_change\",\n  \"conditions\": {\n    \"temperature_c\": {\n      \"gt\": 28\n    },\n    \"humidity_percent\": {\n      \"gt\": 75\n    }\n  }\n}"
            },
            "url": {
              "raw": "{{base_url}}/api/v1/webhooks",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "api",
                "v1",
                "webhooks"
              ]
            },
            "description": "Creates a webhook and saves its ID and one-time HMAC signing secret. Store the secret: list and detail responses do not return it again. A receiver calculates HMAC-SHA256 over timestamp + '.' + the exact raw request body and compares it with X-Webhook-Signature. Change webhook_url to your public HTTPS receiver before running."
          }
        },
        {
          "name": "List webhooks — default (up to 100)",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{base_url}}/api/v1/webhooks",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "api",
                "v1",
                "webhooks"
              ]
            },
            "description": "No page parameters are needed. Pagination is returned only if more than 100 webhooks exist."
          }
        },
        {
          "name": "List webhooks — explicit page",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{base_url}}/api/v1/webhooks?page=1&limit=20",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "api",
                "v1",
                "webhooks"
              ],
              "query": [
                {
                  "key": "page",
                  "value": "1"
                },
                {
                  "key": "limit",
                  "value": "20"
                }
              ]
            },
            "description": "Shows page metadata even when all results fit on one page."
          }
        },
        {
          "name": "List webhooks — selected device",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{base_url}}/api/v1/webhooks?device={{device_mac}}",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "api",
                "v1",
                "webhooks"
              ],
              "query": [
                {
                  "key": "device",
                  "value": "{{device_mac}}"
                }
              ]
            },
            "description": "Returns webhooks assigned to device_mac and organization-wide webhooks whose devices array is empty. Add page and limit when pagination is needed."
          }
        },
        {
          "name": "Get webhook",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{base_url}}/api/v1/webhooks/{{webhook_id}}",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "api",
                "v1",
                "webhooks",
                "{{webhook_id}}"
              ]
            }
          }
        },
        {
          "name": "Pause webhook",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "body": {
              "mode": "raw",
              "raw": "{\n  \"enabled\": false\n}"
            },
            "url": {
              "raw": "{{base_url}}/api/v1/webhooks/{{webhook_id}}",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "api",
                "v1",
                "webhooks",
                "{{webhook_id}}"
              ]
            },
            "description": "Updates only enabled and keeps every other webhook setting unchanged."
          }
        },
        {
          "name": "Resume webhook",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "body": {
              "mode": "raw",
              "raw": "{\n  \"enabled\": true\n}"
            },
            "url": {
              "raw": "{{base_url}}/api/v1/webhooks/{{webhook_id}}",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "api",
                "v1",
                "webhooks",
                "{{webhook_id}}"
              ]
            },
            "description": "Resumes delivery while keeping the URL, filters, and conditions unchanged."
          }
        },
        {
          "name": "Change temperature threshold",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "body": {
              "mode": "raw",
              "raw": "{\n  \"conditions\": {\n    \"temperature_c\": {\n      \"gt\": 30\n    }\n  }\n}"
            },
            "url": {
              "raw": "{{base_url}}/api/v1/webhooks/{{webhook_id}}",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "api",
                "v1",
                "webhooks",
                "{{webhook_id}}"
              ]
            },
            "description": "Replaces the complete conditions object. Other webhook fields remain unchanged."
          }
        },
        {
          "name": "Change devices and events",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "body": {
              "mode": "raw",
              "raw": "{\n  \"devices\": [\n    \"{{device_mac}}\"\n  ],\n  \"events\": [\n    \"boot\",\n    \"reboot\",\n    \"wakeup\",\n    \"sleep\"\n  ]\n}"
            },
            "url": {
              "raw": "{{base_url}}/api/v1/webhooks/{{webhook_id}}",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "api",
                "v1",
                "webhooks",
                "{{webhook_id}}"
              ]
            },
            "description": "Replaces the device and event filters in one update. Use an empty devices array to accept every device."
          }
        },
        {
          "name": "Change destination URL",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "body": {
              "mode": "raw",
              "raw": "{\n  \"url\": \"{{webhook_url}}\"\n}"
            },
            "url": {
              "raw": "{{base_url}}/api/v1/webhooks/{{webhook_id}}",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "api",
                "v1",
                "webhooks",
                "{{webhook_id}}"
              ]
            },
            "description": "Changes only the HTTPS destination URL."
          }
        },
        {
          "name": "Configure Telegram message with live values",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "body": {
              "mode": "raw",
              "raw": "{\n  \"url\": \"https://api.telegram.org/bot{{telegram_bot_token}}/sendMessage?chat_id={{telegram_chat_id}}&text=Temperature:%20{{temperature_c}}%20C%2C%20humidity:%20{{humidity_percent}}%25\",\n  \"mode\": \"on_trigger\",\n  \"conditions\": {\n    \"temperature_c\": {\n      \"gt\": 28\n    }\n  }\n}"
            },
            "url": {
              "raw": "{{base_url}}/api/v1/webhooks/{{webhook_id}}",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "api",
                "v1",
                "webhooks",
                "{{webhook_id}}"
              ]
            },
            "description": "Stores a URL template with literal {{temperature_c}} and {{humidity_percent}} placeholders. Set telegram_bot_token and telegram_chat_id in Postman, but do not define sensor-placeholder variables: the API substitutes those during delivery."
          }
        },
        {
          "name": "Change several webhook settings",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "body": {
              "mode": "raw",
              "raw": "{\n  \"name\": \"Critical climate alert\",\n  \"match\": \"all\",\n  \"mode\": \"on_trigger\",\n  \"conditions\": {\n    \"temperature_c\": {\n      \"gte\": 30\n    },\n    \"humidity_percent\": {\n      \"gt\": 80\n    }\n  },\n  \"enabled\": true\n}"
            },
            "url": {
              "raw": "{{base_url}}/api/v1/webhooks/{{webhook_id}}",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "api",
                "v1",
                "webhooks",
                "{{webhook_id}}"
              ]
            },
            "description": "Demonstrates changing several fields in one request. Fields not included in the body are preserved."
          }
        },
        {
          "name": "Send test webhook",
          "request": {
            "method": "POST",
            "header": [],
            "url": {
              "raw": "{{base_url}}/api/v1/webhooks/{{webhook_id}}/test",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "api",
                "v1",
                "webhooks",
                "{{webhook_id}}",
                "test"
              ]
            },
            "description": "Queues a webhook.test delivery. Enable the webhook before using this request."
          }
        },
        {
          "name": "List delivery attempts",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{base_url}}/api/v1/webhooks/{{webhook_id}}/deliveries",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "api",
                "v1",
                "webhooks",
                "{{webhook_id}}",
                "deliveries"
              ]
            },
            "description": "Returns delivery status, retry count, HTTP status and any error. Add page and limit only when needed."
          }
        },
        {
          "name": "Delete webhook",
          "request": {
            "method": "DELETE",
            "header": [],
            "url": {
              "raw": "{{base_url}}/api/v1/webhooks/{{webhook_id}}",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "api",
                "v1",
                "webhooks",
                "{{webhook_id}}"
              ]
            },
            "description": "Deletes the webhook and cancels pending deliveries. No request body is required."
          }
        },
        {
          "name": "Use ordinary measurements instead of lifecycle events",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": "{{base_url}}/api/v1/webhooks/{{webhook_id}}",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"events\": []\n}"
            },
            "description": "Empty events selects ordinary measurements, including contact position. A non-empty array selects only listed lifecycle events. Omitting events on update preserves the existing filter."
          }
        }
      ]
    }
  ]
}
