{"openapi":"3.1.0","info":{"title":"EasyBoard Public API","version":"1.0.0","description":"API for updating and reading existing EasyBoard dashboards from scripts, automations, and custom apps."},"servers":[{"url":"https://easyboard.live","description":"Production"},{"url":"http://localhost:3000","description":"Local development"}],"tags":[{"name":"Dashboard","description":"Read and update dashboard-level settings like the display name."},{"name":"Tiles","description":"Read, create, update, delete, and reorder tiles on an existing dashboard."},{"name":"Realtime","description":"Subscribe to live dashboard snapshots over Server-Sent Events."}],"components":{"securitySchemes":{"WriteTokenAuth":{"type":"http","scheme":"bearer","bearerFormat":"Write token","description":"Use the dashboard write token for requests that change tiles."}},"schemas":{"Tile":{"type":"object","required":["id","type","title","value","size","order"],"properties":{"id":{"type":"string","example":"tile-abc123"},"alternateId":{"type":"string","description":"Optional Pro-only identifier. Unique within the dashboard.","example":"RevenueTile"},"type":{"type":"string","description":"Built-in tile types: metric, text, counter, progress, clock, countdown, elapsed, toggle.","example":"metric"},"title":{"type":"string","maxLength":200,"example":"Revenue"},"value":{"type":"string","maxLength":10000,"example":"1250"},"unit":{"type":"string","maxLength":32,"example":"$"},"options":{"type":"array","items":{"type":"string"},"description":"Toggle tiles only. The ordered list of values to cycle through.","example":["Open","Closed"]},"size":{"type":"string","description":"Built-in sizes: small, medium, large.","example":"medium"},"order":{"type":"integer","example":0}}},"TileCreateRequest":{"type":"object","properties":{"type":{"type":"string","description":"Use one of the built-in tile types: metric, text, counter, progress, clock, countdown, elapsed, or toggle. Other values may save successfully but may not display correctly in EasyBoard.","example":"metric"},"title":{"type":"string","maxLength":200,"description":"Tile label shown above the value.","example":"Revenue"},"value":{"type":"string","maxLength":10000,"description":"Tile value, always stored as a string.","example":"1250"},"unit":{"type":"string","maxLength":32,"description":"Optional prefix or suffix for metric tiles.","example":"$"},"size":{"type":"string","description":"Use small, medium, or large. Other values may save successfully but may display oddly, so integrations should stick to those three.","example":"medium"},"options":{"type":"array","items":{"type":"string"},"description":"Toggle tiles only. The ordered list of values to cycle through (2–3 entries). The first entry becomes the initial value.","example":["Open","Closed"]},"alternateId":{"type":"string","description":"Optional Pro-only identifier you control. Must be unique within the dashboard and can later be used with `lookup=alternateId` or as a batch update target.","example":"RevenueTile"}}},"TilePatchRequest":{"type":"object","properties":{"title":{"type":"string","maxLength":200,"description":"Rename the tile.","example":"Monthly Revenue"},"value":{"type":"string","maxLength":10000,"description":"Replace the tile value with an absolute string value.","example":"1500"},"unit":{"type":"string","maxLength":32,"description":"Update the tile unit.","example":"$"},"size":{"type":"string","description":"Resize the tile with small, medium, or large. Other values may save successfully but may display oddly.","example":"large"},"type":{"type":"string","description":"Change the tile type to metric, text, counter, progress, clock, countdown, elapsed, or toggle.","example":"progress"},"delta":{"type":"number","description":"Add to the current stored value. This is mainly for counters, but the API currently applies it to any tile whose value can be treated like a number.","example":1},"next":{"type":"boolean","description":"Toggle tiles only. When true, the server advances the current value to the next option in the configured list, wrapping around after the last option. Takes priority over `value` if both are present.","example":true},"options":{"type":"array","items":{"type":"string"},"description":"Toggle tiles only. Update the ordered list of values to cycle through (2–3 entries).","example":["Open","Closed"]},"alternateId":{"type":"string","description":"Optional Pro-only identifier to assign, rename, or clear. Send an empty string to clear it.","example":"RevenueTile"}}},"TileTarget":{"oneOf":[{"type":"object","required":["id"],"properties":{"id":{"type":"string","description":"Target a tile by its generated tile ID.","example":"tile-abc123"}}},{"type":"object","required":["alternateId"],"properties":{"alternateId":{"type":"string","description":"Target a tile by its Pro-only alternateId.","example":"RevenueTile"}}}]},"TileBatchUpdateInstruction":{"type":"object","required":["target","patch"],"properties":{"target":{"$ref":"#/components/schemas/TileTarget"},"patch":{"$ref":"#/components/schemas/TilePatchRequest"}}},"TileBatchPatchRequest":{"type":"object","required":["updates"],"properties":{"updates":{"type":"array","minItems":1,"maxItems":100,"description":"One or more tile updates applied atomically. If any instruction fails, no tile changes are saved.","items":{"$ref":"#/components/schemas/TileBatchUpdateInstruction"}}}},"TileBatchPatchResponse":{"type":"object","required":["updated"],"properties":{"updated":{"type":"array","description":"Updated tiles in the same order the instructions were sent.","items":{"$ref":"#/components/schemas/Tile"}}}},"ReorderRequest":{"type":"object","required":["ids"],"properties":{"ids":{"type":"array","description":"Ordered list of tile IDs. The first element becomes order 0.","items":{"type":"string"},"example":["tile-def456","tile-abc123"]}}},"DashboardSnapshot":{"type":"object","required":["tiles"],"properties":{"name":{"type":"string","description":"Display name of the dashboard. May be absent if no name has been set.","example":"Coffee Shop Sales"},"tiles":{"type":"array","items":{"$ref":"#/components/schemas/Tile"}}}},"DashboardPatchRequest":{"type":"object","properties":{"name":{"type":"string","maxLength":100,"description":"Display name for the dashboard. Emojis are welcome. Send an empty string to clear.","example":"Coffee Shop Sales"}}},"DashboardPatchResponse":{"type":"object","properties":{"name":{"type":"string","description":"The updated dashboard name.","example":"Coffee Shop Sales"}}},"ErrorResponse":{"type":"object","required":["error"],"properties":{"error":{"type":"string","example":"Unauthorized"}}}}},"paths":{"/api/d/{dashboardId}":{"patch":{"operationId":"updateDashboard","summary":"Update dashboard settings","description":"Updates dashboard-level settings. Currently supports renaming the dashboard. The name appears in the header and browser tab for all viewers, and is broadcast to connected SSE clients in real time.","tags":["Dashboard"],"parameters":[{"name":"dashboardId","in":"path","required":true,"description":"The 12-character dashboard ID from your dashboard URL.","schema":{"type":"string","example":"abc123xyz456"}}],"security":[{"WriteTokenAuth":[]}],"requestBody":{"required":true,"description":"Send the fields you want to update.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DashboardPatchRequest"},"example":{"name":"Coffee Shop Sales"}}}},"responses":{"200":{"description":"Updated dashboard settings.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DashboardPatchResponse"},"example":{"name":"Coffee Shop Sales"}}}},"400":{"description":"Invalid body or name too long.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"},"example":{"error":"name must be 100 characters or fewer"}}}},"401":{"description":"Missing or incorrect write token.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"},"example":{"error":"Unauthorized"}}}},"404":{"description":"Dashboard not found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"},"example":{"error":"Dashboard not found"}}}}},"x-codeSamples":[{"lang":"curl","label":"curl","source":"curl -X PATCH https://easyboard.live/api/d/YOUR_DASHBOARD_ID \\\n  -H \"Authorization: Bearer YOUR_WRITE_TOKEN\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"name\":\"Coffee Shop Sales\"}'"},{"lang":"c","label":"C","source":"/* Compile with: cc example.c -lcurl */\n#include <curl/curl.h>\n#include <stdio.h>\n\nstatic size_t write_callback(char *ptr, size_t size, size_t nmemb, void *userdata) {\n  size_t total = size * nmemb;\n  (void)userdata;\n  fwrite(ptr, 1, total, stdout);\n  return total;\n}\n\nint main(void) {\n  if (curl_global_init(CURL_GLOBAL_DEFAULT) != CURLE_OK) return 1;\n\n  CURL *curl = curl_easy_init();\n  if (!curl) {\n    curl_global_cleanup();\n    return 1;\n  }\n  struct curl_slist *headers = NULL;\n  const char *json = \"{\\\"name\\\":\\\"Coffee Shop Sales\\\"}\";\n\n  curl_easy_setopt(curl, CURLOPT_URL, \"https://easyboard.live/api/d/YOUR_DASHBOARD_ID\");\n  curl_easy_setopt(curl, CURLOPT_CUSTOMREQUEST, \"PATCH\");\n  headers = curl_slist_append(headers, \"Authorization: Bearer YOUR_WRITE_TOKEN\");\n  headers = curl_slist_append(headers, \"Content-Type: application/json\");\n  curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers);\n  curl_easy_setopt(curl, CURLOPT_POSTFIELDS, json);\n  curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, write_callback);\n\n  CURLcode res = curl_easy_perform(curl);\n  if (res != CURLE_OK) {\n    fprintf(stderr, \"request failed: %s\\n\", curl_easy_strerror(res));\n  }\n\n  curl_slist_free_all(headers);\n  curl_easy_cleanup(curl);\n  curl_global_cleanup();\n  return res == CURLE_OK ? 0 : 1;\n}"},{"lang":"javascript","label":"JavaScript","source":"await fetch(\n  'https://easyboard.live/api/d/YOUR_DASHBOARD_ID',\n  {\n    method: 'PATCH',\n    headers: {\n      Authorization: 'Bearer YOUR_WRITE_TOKEN',\n      'Content-Type': 'application/json',\n    },\n    body: JSON.stringify({ name: 'Coffee Shop Sales' }),\n  }\n);"},{"lang":"python","label":"Python","source":"import requests\n\nrequests.patch(\n    'https://easyboard.live/api/d/YOUR_DASHBOARD_ID',\n    headers={\n        'Authorization': 'Bearer YOUR_WRITE_TOKEN',\n        'Content-Type': 'application/json',\n    },\n    json={'name': 'Coffee Shop Sales'},\n)"},{"lang":"powershell","label":"PowerShell","source":"Invoke-RestMethod https://easyboard.live/api/d/YOUR_DASHBOARD_ID `\n  -Method Patch `\n  -Headers @{ Authorization = 'Bearer YOUR_WRITE_TOKEN' } `\n  -ContentType 'application/json' `\n  -Body '{\"name\":\"Coffee Shop Sales\"}'"},{"lang":"arduino","label":"Arduino (ESP32)","source":"/*\n * ESP32 Arduino — HTTPS request\n * Board: \"ESP32 Dev Module\" in Arduino IDE / PlatformIO\n * Library: ArduinoJson (install via Library Manager)\n *\n * HTTPS note: setInsecure() skips certificate verification.\n * For production, pin the root CA — see the comment below.\n */\n#include <WiFi.h>\n#include <WiFiClientSecure.h>\n#include <HTTPClient.h>\n\nconst char* ssid     = \"YOUR_WIFI_SSID\";\nconst char* password = \"YOUR_WIFI_PASSWORD\";\n\n/*\n * Production CA pinning: replace setInsecure() with setCACert().\n * 1. Visit https://easyboard.live in a browser.\n * 2. Click the padlock → Certificate → Details → Export the Root CA\n *    (ISRG Root X1 for Let's Encrypt) as PEM.\n * 3. Paste the PEM content into a const char* here:\n *\n *    const char* rootCA = \\\n *      \"-----BEGIN CERTIFICATE-----\\n\"\n *      \"MIIFaz...\\n\"\n *      \"-----END CERTIFICATE-----\\n\";\n *\n * 4. Replace client.setInsecure() with client.setCACert(rootCA);\n */\n\nvoid setup() {\n  Serial.begin(115200);\n  WiFi.begin(ssid, password);\n  while (WiFi.status() != WL_CONNECTED) {\n    delay(500);\n    Serial.print(\".\");\n  }\n  Serial.println(\"\\nWiFi connected\");\n\n  WiFiClientSecure client;\n  client.setInsecure(); // For testing only — see CA pinning note above\n\n  HTTPClient http;\n  http.begin(client, \"https://easyboard.live/api/d/YOUR_DASHBOARD_ID\");\n  http.addHeader(\"Authorization\", \"Bearer YOUR_WRITE_TOKEN\");\n  http.addHeader(\"Content-Type\", \"application/json\");\n\n  String json = \"{\\\"name\\\":\\\"Coffee Shop Sales\\\"}\";\n\n  int code = http.sendRequest(\"PATCH\", json);\n\n  if (code > 0) {\n    Serial.printf(\"HTTP %d\\n\", code);\n    Serial.println(http.getString());\n  } else {\n    Serial.printf(\"Request failed: %s\\n\", http.errorToString(code).c_str());\n  }\n\n  http.end();\n}\n\nvoid loop() {\n  // Runs once. Add delay + repeat here for periodic updates.\n}"}]}},"/api/d/{dashboardId}/tiles":{"get":{"operationId":"listTiles","summary":"List tiles","description":"Returns the current tiles for a dashboard, sorted by order. You can call this without a write token.","tags":["Tiles"],"parameters":[{"name":"dashboardId","in":"path","required":true,"description":"The 12-character dashboard ID from your dashboard URL.","schema":{"type":"string","example":"abc123xyz456"}}],"responses":{"200":{"description":"Sorted tile array.","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/Tile"}},"example":[{"id":"tile-abc123","alternateId":"RevenueTile","type":"metric","title":"Revenue","value":"1250","unit":"$","size":"medium","order":0},{"id":"tile-def456","type":"counter","title":"Orders","value":"12","unit":"","size":"small","order":1}]}}},"404":{"description":"Dashboard not found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"},"example":{"error":"Dashboard not found"}}}}},"x-codeSamples":[{"lang":"curl","label":"curl","source":"curl https://easyboard.live/api/d/YOUR_DASHBOARD_ID/tiles"},{"lang":"c","label":"C","source":"/* Compile with: cc example.c -lcurl */\n#include <curl/curl.h>\n#include <stdio.h>\n\nstatic size_t write_callback(char *ptr, size_t size, size_t nmemb, void *userdata) {\n  size_t total = size * nmemb;\n  (void)userdata;\n  fwrite(ptr, 1, total, stdout);\n  return total;\n}\n\nint main(void) {\n  if (curl_global_init(CURL_GLOBAL_DEFAULT) != CURLE_OK) return 1;\n\n  CURL *curl = curl_easy_init();\n  if (!curl) {\n    curl_global_cleanup();\n    return 1;\n  }\n  curl_easy_setopt(curl, CURLOPT_URL, \"https://easyboard.live/api/d/YOUR_DASHBOARD_ID/tiles\");\n  curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, write_callback);\n\n  CURLcode res = curl_easy_perform(curl);\n  if (res != CURLE_OK) {\n    fprintf(stderr, \"request failed: %s\\n\", curl_easy_strerror(res));\n  }\n\n  curl_easy_cleanup(curl);\n  curl_global_cleanup();\n  return res == CURLE_OK ? 0 : 1;\n}"},{"lang":"javascript","label":"JavaScript","source":"const res = await fetch(\n  'https://easyboard.live/api/d/YOUR_DASHBOARD_ID/tiles'\n);\n\nconst tiles = await res.json();\nconsole.log(tiles);"},{"lang":"python","label":"Python","source":"import requests\n\nres = requests.get(\n    'https://easyboard.live/api/d/YOUR_DASHBOARD_ID/tiles'\n)\n\nprint(res.json())"},{"lang":"powershell","label":"PowerShell","source":"Invoke-RestMethod https://easyboard.live/api/d/YOUR_DASHBOARD_ID/tiles"},{"lang":"arduino","label":"Arduino (ESP32)","source":"/*\n * ESP32 Arduino — HTTPS request\n * Board: \"ESP32 Dev Module\" in Arduino IDE / PlatformIO\n *\n * HTTPS note: setInsecure() skips certificate verification.\n * For production, pin the root CA — see the comment below.\n */\n#include <WiFi.h>\n#include <WiFiClientSecure.h>\n#include <HTTPClient.h>\n\nconst char* ssid     = \"YOUR_WIFI_SSID\";\nconst char* password = \"YOUR_WIFI_PASSWORD\";\n\n/*\n * Production CA pinning: replace setInsecure() with setCACert().\n * 1. Visit https://easyboard.live in a browser.\n * 2. Click the padlock → Certificate → Details → Export the Root CA\n *    (ISRG Root X1 for Let's Encrypt) as PEM.\n * 3. Paste the PEM content into a const char* here:\n *\n *    const char* rootCA = \\\n *      \"-----BEGIN CERTIFICATE-----\\n\"\n *      \"MIIFaz...\\n\"\n *      \"-----END CERTIFICATE-----\\n\";\n *\n * 4. Replace client.setInsecure() with client.setCACert(rootCA);\n */\n\nvoid setup() {\n  Serial.begin(115200);\n  WiFi.begin(ssid, password);\n  while (WiFi.status() != WL_CONNECTED) {\n    delay(500);\n    Serial.print(\".\");\n  }\n  Serial.println(\"\\nWiFi connected\");\n\n  WiFiClientSecure client;\n  client.setInsecure(); // For testing only — see CA pinning note above\n\n  HTTPClient http;\n  http.begin(client, \"https://easyboard.live/api/d/YOUR_DASHBOARD_ID/tiles\");\n\n  int code = http.GET();\n\n  if (code > 0) {\n    Serial.printf(\"HTTP %d\\n\", code);\n    Serial.println(http.getString());\n  } else {\n    Serial.printf(\"Request failed: %s\\n\", http.errorToString(code).c_str());\n  }\n\n  http.end();\n}\n\nvoid loop() {\n  // Runs once. Add delay + repeat here for periodic updates.\n}"}],"x-easyboardRecipes":[{"id":"read-all-tiles","title":"Read all tiles","description":"Fetch a one-shot snapshot of the dashboard without opening a streaming connection.","samples":[{"lang":"curl","label":"curl","source":"curl https://easyboard.live/api/d/YOUR_DASHBOARD_ID/tiles"},{"lang":"c","label":"C","source":"/* Compile with: cc example.c -lcurl */\n#include <curl/curl.h>\n#include <stdio.h>\n\nstatic size_t write_callback(char *ptr, size_t size, size_t nmemb, void *userdata) {\n  size_t total = size * nmemb;\n  (void)userdata;\n  fwrite(ptr, 1, total, stdout);\n  return total;\n}\n\nint main(void) {\n  if (curl_global_init(CURL_GLOBAL_DEFAULT) != CURLE_OK) return 1;\n\n  CURL *curl = curl_easy_init();\n  if (!curl) {\n    curl_global_cleanup();\n    return 1;\n  }\n  curl_easy_setopt(curl, CURLOPT_URL, \"https://easyboard.live/api/d/YOUR_DASHBOARD_ID/tiles\");\n  curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, write_callback);\n\n  CURLcode res = curl_easy_perform(curl);\n  if (res != CURLE_OK) {\n    fprintf(stderr, \"request failed: %s\\n\", curl_easy_strerror(res));\n  }\n\n  curl_easy_cleanup(curl);\n  curl_global_cleanup();\n  return res == CURLE_OK ? 0 : 1;\n}"},{"lang":"javascript","label":"JavaScript","source":"const res = await fetch(\n  'https://easyboard.live/api/d/YOUR_DASHBOARD_ID/tiles'\n);\n\nconst tiles = await res.json();\nconsole.log(tiles);"},{"lang":"python","label":"Python","source":"import requests\n\nres = requests.get(\n    'https://easyboard.live/api/d/YOUR_DASHBOARD_ID/tiles'\n)\n\nprint(res.json())"},{"lang":"powershell","label":"PowerShell","source":"Invoke-RestMethod https://easyboard.live/api/d/YOUR_DASHBOARD_ID/tiles"},{"lang":"arduino","label":"Arduino (ESP32)","source":"/*\n * ESP32 Arduino — HTTPS request\n * Board: \"ESP32 Dev Module\" in Arduino IDE / PlatformIO\n *\n * HTTPS note: setInsecure() skips certificate verification.\n * For production, pin the root CA — see the comment below.\n */\n#include <WiFi.h>\n#include <WiFiClientSecure.h>\n#include <HTTPClient.h>\n\nconst char* ssid     = \"YOUR_WIFI_SSID\";\nconst char* password = \"YOUR_WIFI_PASSWORD\";\n\n/*\n * Production CA pinning: replace setInsecure() with setCACert().\n * 1. Visit https://easyboard.live in a browser.\n * 2. Click the padlock → Certificate → Details → Export the Root CA\n *    (ISRG Root X1 for Let's Encrypt) as PEM.\n * 3. Paste the PEM content into a const char* here:\n *\n *    const char* rootCA = \\\n *      \"-----BEGIN CERTIFICATE-----\\n\"\n *      \"MIIFaz...\\n\"\n *      \"-----END CERTIFICATE-----\\n\";\n *\n * 4. Replace client.setInsecure() with client.setCACert(rootCA);\n */\n\nvoid setup() {\n  Serial.begin(115200);\n  WiFi.begin(ssid, password);\n  while (WiFi.status() != WL_CONNECTED) {\n    delay(500);\n    Serial.print(\".\");\n  }\n  Serial.println(\"\\nWiFi connected\");\n\n  WiFiClientSecure client;\n  client.setInsecure(); // For testing only — see CA pinning note above\n\n  HTTPClient http;\n  http.begin(client, \"https://easyboard.live/api/d/YOUR_DASHBOARD_ID/tiles\");\n\n  int code = http.GET();\n\n  if (code > 0) {\n    Serial.printf(\"HTTP %d\\n\", code);\n    Serial.println(http.getString());\n  } else {\n    Serial.printf(\"Request failed: %s\\n\", http.errorToString(code).c_str());\n  }\n\n  http.end();\n}\n\nvoid loop() {\n  // Runs once. Add delay + repeat here for periodic updates.\n}"}]}]},"post":{"operationId":"createTile","summary":"Create a tile","description":"Creates a new tile at the end of the dashboard. Send your dashboard write token in the Authorization header.","tags":["Tiles"],"parameters":[{"name":"dashboardId","in":"path","required":true,"description":"The 12-character dashboard ID from your dashboard URL.","schema":{"type":"string","example":"abc123xyz456"}}],"security":[{"WriteTokenAuth":[]}],"requestBody":{"required":true,"description":"Tile values are always stored as strings.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TileCreateRequest"},"example":{"type":"metric","title":"Revenue","value":"1250","unit":"$","size":"medium"}}}},"responses":{"201":{"description":"Created tile.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Tile"},"example":{"id":"tile-abc123","alternateId":"RevenueTile","type":"metric","title":"Revenue","value":"1250","unit":"$","size":"medium","order":0}}}},"400":{"description":"Invalid JSON body or field length violation.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"},"example":{"error":"title must be 200 characters or fewer"}}}},"401":{"description":"Missing or incorrect write token.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"},"example":{"error":"Unauthorized"}}}},"404":{"description":"Dashboard not found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"},"example":{"error":"Dashboard not found"}}}}},"x-codeSamples":[{"lang":"curl","label":"curl","source":"curl -X POST https://easyboard.live/api/d/YOUR_DASHBOARD_ID/tiles \\\n  -H \"Authorization: Bearer YOUR_WRITE_TOKEN\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"type\":\"metric\",\"title\":\"Revenue\",\"value\":\"1250\",\"unit\":\"$\",\"size\":\"medium\"}'"},{"lang":"c","label":"C","source":"/* Compile with: cc example.c -lcurl */\n#include <curl/curl.h>\n#include <stdio.h>\n\nstatic size_t write_callback(char *ptr, size_t size, size_t nmemb, void *userdata) {\n  size_t total = size * nmemb;\n  (void)userdata;\n  fwrite(ptr, 1, total, stdout);\n  return total;\n}\n\nint main(void) {\n  if (curl_global_init(CURL_GLOBAL_DEFAULT) != CURLE_OK) return 1;\n\n  CURL *curl = curl_easy_init();\n  if (!curl) {\n    curl_global_cleanup();\n    return 1;\n  }\n  struct curl_slist *headers = NULL;\n  const char *json = \"{\\\"type\\\":\\\"metric\\\",\\\"title\\\":\\\"Revenue\\\",\\\"value\\\":\\\"1250\\\",\\\"unit\\\":\\\"$\\\",\\\"size\\\":\\\"medium\\\"}\";\n\n  curl_easy_setopt(curl, CURLOPT_URL, \"https://easyboard.live/api/d/YOUR_DASHBOARD_ID/tiles\");\n  curl_easy_setopt(curl, CURLOPT_CUSTOMREQUEST, \"POST\");\n  headers = curl_slist_append(headers, \"Authorization: Bearer YOUR_WRITE_TOKEN\");\n  headers = curl_slist_append(headers, \"Content-Type: application/json\");\n  curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers);\n  curl_easy_setopt(curl, CURLOPT_POSTFIELDS, json);\n  curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, write_callback);\n\n  CURLcode res = curl_easy_perform(curl);\n  if (res != CURLE_OK) {\n    fprintf(stderr, \"request failed: %s\\n\", curl_easy_strerror(res));\n  }\n\n  curl_slist_free_all(headers);\n  curl_easy_cleanup(curl);\n  curl_global_cleanup();\n  return res == CURLE_OK ? 0 : 1;\n}"},{"lang":"javascript","label":"JavaScript","source":"const res = await fetch(\n  'https://easyboard.live/api/d/YOUR_DASHBOARD_ID/tiles',\n  {\n    method: 'POST',\n    headers: {\n      Authorization: 'Bearer YOUR_WRITE_TOKEN',\n      'Content-Type': 'application/json',\n    },\n    body: JSON.stringify({\n      type: 'metric',\n      title: 'Revenue',\n      value: '1250',\n      unit: '$',\n      size: 'medium',\n    }),\n  }\n);\n\nconst tile = await res.json();\nconsole.log(tile);"},{"lang":"python","label":"Python","source":"import requests\n\nres = requests.post(\n    'https://easyboard.live/api/d/YOUR_DASHBOARD_ID/tiles',\n    headers={\n        'Authorization': 'Bearer YOUR_WRITE_TOKEN',\n        'Content-Type': 'application/json',\n    },\n    json={\n        'type': 'metric',\n        'title': 'Revenue',\n        'value': '1250',\n        'unit': '$',\n        'size': 'medium',\n    },\n)\n\nprint(res.json())"},{"lang":"powershell","label":"PowerShell","source":"Invoke-RestMethod https://easyboard.live/api/d/YOUR_DASHBOARD_ID/tiles `\n  -Method Post `\n  -Headers @{ Authorization = 'Bearer YOUR_WRITE_TOKEN' } `\n  -ContentType 'application/json' `\n  -Body '{\"type\":\"metric\",\"title\":\"Revenue\",\"value\":\"1250\",\"unit\":\"$\",\"size\":\"medium\"}'"},{"lang":"arduino","label":"Arduino (ESP32)","source":"/*\n * ESP32 Arduino — HTTPS request\n * Board: \"ESP32 Dev Module\" in Arduino IDE / PlatformIO\n * Library: ArduinoJson (install via Library Manager)\n *\n * HTTPS note: setInsecure() skips certificate verification.\n * For production, pin the root CA — see the comment below.\n */\n#include <WiFi.h>\n#include <WiFiClientSecure.h>\n#include <HTTPClient.h>\n\nconst char* ssid     = \"YOUR_WIFI_SSID\";\nconst char* password = \"YOUR_WIFI_PASSWORD\";\n\n/*\n * Production CA pinning: replace setInsecure() with setCACert().\n * 1. Visit https://easyboard.live in a browser.\n * 2. Click the padlock → Certificate → Details → Export the Root CA\n *    (ISRG Root X1 for Let's Encrypt) as PEM.\n * 3. Paste the PEM content into a const char* here:\n *\n *    const char* rootCA = \\\n *      \"-----BEGIN CERTIFICATE-----\\n\"\n *      \"MIIFaz...\\n\"\n *      \"-----END CERTIFICATE-----\\n\";\n *\n * 4. Replace client.setInsecure() with client.setCACert(rootCA);\n */\n\nvoid setup() {\n  Serial.begin(115200);\n  WiFi.begin(ssid, password);\n  while (WiFi.status() != WL_CONNECTED) {\n    delay(500);\n    Serial.print(\".\");\n  }\n  Serial.println(\"\\nWiFi connected\");\n\n  WiFiClientSecure client;\n  client.setInsecure(); // For testing only — see CA pinning note above\n\n  HTTPClient http;\n  http.begin(client, \"https://easyboard.live/api/d/YOUR_DASHBOARD_ID/tiles\");\n  http.addHeader(\"Authorization\", \"Bearer YOUR_WRITE_TOKEN\");\n  http.addHeader(\"Content-Type\", \"application/json\");\n\n  String json = \"{\\\"type\\\":\\\"metric\\\",\\\"title\\\":\\\"Revenue\\\",\\\"value\\\":\\\"1250\\\",\\\"unit\\\":\\\"$\\\",\\\"size\\\":\\\"medium\\\"}\";\n\n  int code = http.POST(json);\n\n  if (code > 0) {\n    Serial.printf(\"HTTP %d\\n\", code);\n    Serial.println(http.getString());\n  } else {\n    Serial.printf(\"Request failed: %s\\n\", http.errorToString(code).c_str());\n  }\n\n  http.end();\n}\n\nvoid loop() {\n  // Runs once. Add delay + repeat here for periodic updates.\n}"}],"x-easyboardRecipes":[{"id":"create-metric-tile","title":"Create a metric tile","description":"Add a new metric tile at the end of the dashboard.","samples":[{"lang":"curl","label":"curl","source":"curl -X POST https://easyboard.live/api/d/YOUR_DASHBOARD_ID/tiles \\\n  -H \"Authorization: Bearer YOUR_WRITE_TOKEN\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"type\":\"metric\",\"title\":\"Revenue\",\"value\":\"1250\",\"unit\":\"$\",\"size\":\"medium\"}'"},{"lang":"c","label":"C","source":"/* Compile with: cc example.c -lcurl */\n#include <curl/curl.h>\n#include <stdio.h>\n\nstatic size_t write_callback(char *ptr, size_t size, size_t nmemb, void *userdata) {\n  size_t total = size * nmemb;\n  (void)userdata;\n  fwrite(ptr, 1, total, stdout);\n  return total;\n}\n\nint main(void) {\n  if (curl_global_init(CURL_GLOBAL_DEFAULT) != CURLE_OK) return 1;\n\n  CURL *curl = curl_easy_init();\n  if (!curl) {\n    curl_global_cleanup();\n    return 1;\n  }\n  struct curl_slist *headers = NULL;\n  const char *json = \"{\\\"type\\\":\\\"metric\\\",\\\"title\\\":\\\"Revenue\\\",\\\"value\\\":\\\"1250\\\",\\\"unit\\\":\\\"$\\\",\\\"size\\\":\\\"medium\\\"}\";\n\n  curl_easy_setopt(curl, CURLOPT_URL, \"https://easyboard.live/api/d/YOUR_DASHBOARD_ID/tiles\");\n  curl_easy_setopt(curl, CURLOPT_CUSTOMREQUEST, \"POST\");\n  headers = curl_slist_append(headers, \"Authorization: Bearer YOUR_WRITE_TOKEN\");\n  headers = curl_slist_append(headers, \"Content-Type: application/json\");\n  curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers);\n  curl_easy_setopt(curl, CURLOPT_POSTFIELDS, json);\n  curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, write_callback);\n\n  CURLcode res = curl_easy_perform(curl);\n  if (res != CURLE_OK) {\n    fprintf(stderr, \"request failed: %s\\n\", curl_easy_strerror(res));\n  }\n\n  curl_slist_free_all(headers);\n  curl_easy_cleanup(curl);\n  curl_global_cleanup();\n  return res == CURLE_OK ? 0 : 1;\n}"},{"lang":"javascript","label":"JavaScript","source":"await fetch('https://easyboard.live/api/d/YOUR_DASHBOARD_ID/tiles', {\n  method: 'POST',\n  headers: {\n    Authorization: 'Bearer YOUR_WRITE_TOKEN',\n    'Content-Type': 'application/json',\n  },\n  body: JSON.stringify({\n    type: 'metric',\n    title: 'Revenue',\n    value: '1250',\n    unit: '$',\n    size: 'medium',\n  }),\n});"},{"lang":"python","label":"Python","source":"import requests\n\nrequests.post(\n    'https://easyboard.live/api/d/YOUR_DASHBOARD_ID/tiles',\n    headers={\n        'Authorization': 'Bearer YOUR_WRITE_TOKEN',\n        'Content-Type': 'application/json',\n    },\n    json={\n        'type': 'metric',\n        'title': 'Revenue',\n        'value': '1250',\n        'unit': '$',\n        'size': 'medium',\n    },\n)"},{"lang":"powershell","label":"PowerShell","source":"Invoke-RestMethod https://easyboard.live/api/d/YOUR_DASHBOARD_ID/tiles `\n  -Method Post `\n  -Headers @{ Authorization = 'Bearer YOUR_WRITE_TOKEN' } `\n  -ContentType 'application/json' `\n  -Body '{\"type\":\"metric\",\"title\":\"Revenue\",\"value\":\"1250\",\"unit\":\"$\",\"size\":\"medium\"}'"},{"lang":"arduino","label":"Arduino (ESP32)","source":"/*\n * ESP32 Arduino — HTTPS request\n * Board: \"ESP32 Dev Module\" in Arduino IDE / PlatformIO\n * Library: ArduinoJson (install via Library Manager)\n *\n * HTTPS note: setInsecure() skips certificate verification.\n * For production, pin the root CA — see the comment below.\n */\n#include <WiFi.h>\n#include <WiFiClientSecure.h>\n#include <HTTPClient.h>\n\nconst char* ssid     = \"YOUR_WIFI_SSID\";\nconst char* password = \"YOUR_WIFI_PASSWORD\";\n\n/*\n * Production CA pinning: replace setInsecure() with setCACert().\n * 1. Visit https://easyboard.live in a browser.\n * 2. Click the padlock → Certificate → Details → Export the Root CA\n *    (ISRG Root X1 for Let's Encrypt) as PEM.\n * 3. Paste the PEM content into a const char* here:\n *\n *    const char* rootCA = \\\n *      \"-----BEGIN CERTIFICATE-----\\n\"\n *      \"MIIFaz...\\n\"\n *      \"-----END CERTIFICATE-----\\n\";\n *\n * 4. Replace client.setInsecure() with client.setCACert(rootCA);\n */\n\nvoid setup() {\n  Serial.begin(115200);\n  WiFi.begin(ssid, password);\n  while (WiFi.status() != WL_CONNECTED) {\n    delay(500);\n    Serial.print(\".\");\n  }\n  Serial.println(\"\\nWiFi connected\");\n\n  WiFiClientSecure client;\n  client.setInsecure(); // For testing only — see CA pinning note above\n\n  HTTPClient http;\n  http.begin(client, \"https://easyboard.live/api/d/YOUR_DASHBOARD_ID/tiles\");\n  http.addHeader(\"Authorization\", \"Bearer YOUR_WRITE_TOKEN\");\n  http.addHeader(\"Content-Type\", \"application/json\");\n\n  String json = \"{\\\"type\\\":\\\"metric\\\",\\\"title\\\":\\\"Revenue\\\",\\\"value\\\":\\\"1250\\\",\\\"unit\\\":\\\"$\\\",\\\"size\\\":\\\"medium\\\"}\";\n\n  int code = http.POST(json);\n\n  if (code > 0) {\n    Serial.printf(\"HTTP %d\\n\", code);\n    Serial.println(http.getString());\n  } else {\n    Serial.printf(\"Request failed: %s\\n\", http.errorToString(code).c_str());\n  }\n\n  http.end();\n}\n\nvoid loop() {\n  // Runs once. Add delay + repeat here for periodic updates.\n}"}]}]},"patch":{"operationId":"updateTile","summary":"Update one or more tiles","description":"Applies one or more tile updates atomically. Each instruction targets exactly one tile by `id` or `alternateId`. Current server behavior is important here: if you send both `delta` and `value`, `delta` wins and `value` is ignored. `alternateId` is Pro-only when you set, rename, or clear it.","tags":["Tiles"],"parameters":[{"name":"dashboardId","in":"path","required":true,"description":"The 12-character dashboard ID from your dashboard URL.","schema":{"type":"string","example":"abc123xyz456"}}],"security":[{"WriteTokenAuth":[]}],"requestBody":{"required":true,"description":"Send an `updates` array. Every instruction must include exactly one target and one patch object.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TileBatchPatchRequest"},"example":{"updates":[{"target":{"id":"tile-abc123"},"patch":{"value":"1500"}}]}}}},"responses":{"200":{"description":"Updated tiles in request order.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TileBatchPatchResponse"},"example":{"updated":[{"id":"tile-abc123","alternateId":"RevenueTile","type":"metric","title":"Revenue","value":"1500","unit":"$","size":"medium","order":0}]}}}},"400":{"description":"Invalid JSON body, malformed target, or field length violation.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"},"example":{"error":"Body must be an object with an updates array"}}}},"401":{"description":"Missing or incorrect write token.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"},"example":{"error":"Unauthorized"}}}},"403":{"description":"A Pro-only alternateId write was attempted on a FREE dashboard.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"},"example":{"error":"alternateId is a Pro feature. Upgrade to Pro to set a custom tile identifier."}}}},"404":{"description":"Dashboard or tile not found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"},"example":{"error":"Tile not found"}}}},"409":{"description":"The requested alternateId already exists on this dashboard.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"},"example":{"error":"alternateId is already used on this dashboard"}}}}},"x-codeSamples":[{"lang":"curl","label":"curl","source":"curl -X PATCH https://easyboard.live/api/d/YOUR_DASHBOARD_ID/tiles \\\n  -H \"Authorization: Bearer YOUR_WRITE_TOKEN\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"updates\":[{\"target\":{\"id\":\"YOUR_TILE_ID\"},\"patch\":{\"value\":\"1500\"}}]}'"},{"lang":"c","label":"C","source":"/* Compile with: cc example.c -lcurl */\n#include <curl/curl.h>\n#include <stdio.h>\n\nstatic size_t write_callback(char *ptr, size_t size, size_t nmemb, void *userdata) {\n  size_t total = size * nmemb;\n  (void)userdata;\n  fwrite(ptr, 1, total, stdout);\n  return total;\n}\n\nint main(void) {\n  if (curl_global_init(CURL_GLOBAL_DEFAULT) != CURLE_OK) return 1;\n\n  CURL *curl = curl_easy_init();\n  if (!curl) {\n    curl_global_cleanup();\n    return 1;\n  }\n  struct curl_slist *headers = NULL;\n  const char *json = \"{\\\"updates\\\":[{\\\"target\\\":{\\\"id\\\":\\\"YOUR_TILE_ID\\\"},\\\"patch\\\":{\\\"value\\\":\\\"1500\\\"}}]}\";\n\n  curl_easy_setopt(curl, CURLOPT_URL, \"https://easyboard.live/api/d/YOUR_DASHBOARD_ID/tiles\");\n  curl_easy_setopt(curl, CURLOPT_CUSTOMREQUEST, \"PATCH\");\n  headers = curl_slist_append(headers, \"Authorization: Bearer YOUR_WRITE_TOKEN\");\n  headers = curl_slist_append(headers, \"Content-Type: application/json\");\n  curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers);\n  curl_easy_setopt(curl, CURLOPT_POSTFIELDS, json);\n  curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, write_callback);\n\n  CURLcode res = curl_easy_perform(curl);\n  if (res != CURLE_OK) {\n    fprintf(stderr, \"request failed: %s\\n\", curl_easy_strerror(res));\n  }\n\n  curl_slist_free_all(headers);\n  curl_easy_cleanup(curl);\n  curl_global_cleanup();\n  return res == CURLE_OK ? 0 : 1;\n}"},{"lang":"javascript","label":"JavaScript","source":"await fetch(\n  'https://easyboard.live/api/d/YOUR_DASHBOARD_ID/tiles',\n  {\n    method: 'PATCH',\n    headers: {\n      Authorization: 'Bearer YOUR_WRITE_TOKEN',\n      'Content-Type': 'application/json',\n    },\n    body: JSON.stringify({\n      updates: [{ target: { id: 'YOUR_TILE_ID' }, patch: { value: '1500' } }],\n    }),\n  }\n);"},{"lang":"python","label":"Python","source":"import requests\n\nrequests.patch(\n    'https://easyboard.live/api/d/YOUR_DASHBOARD_ID/tiles',\n    headers={\n        'Authorization': 'Bearer YOUR_WRITE_TOKEN',\n        'Content-Type': 'application/json',\n    },\n    json={\n        'updates': [{'target': {'id': 'YOUR_TILE_ID'}, 'patch': {'value': '1500'}}],\n    },\n)"},{"lang":"powershell","label":"PowerShell","source":"Invoke-RestMethod https://easyboard.live/api/d/YOUR_DASHBOARD_ID/tiles `\n  -Method Patch `\n  -Headers @{ Authorization = 'Bearer YOUR_WRITE_TOKEN' } `\n  -ContentType 'application/json' `\n  -Body '{\"updates\":[{\"target\":{\"id\":\"YOUR_TILE_ID\"},\"patch\":{\"value\":\"1500\"}}]}'"},{"lang":"arduino","label":"Arduino (ESP32)","source":"/*\n * ESP32 Arduino — HTTPS request\n * Board: \"ESP32 Dev Module\" in Arduino IDE / PlatformIO\n * Library: ArduinoJson (install via Library Manager)\n *\n * HTTPS note: setInsecure() skips certificate verification.\n * For production, pin the root CA — see the comment below.\n */\n#include <WiFi.h>\n#include <WiFiClientSecure.h>\n#include <HTTPClient.h>\n\nconst char* ssid     = \"YOUR_WIFI_SSID\";\nconst char* password = \"YOUR_WIFI_PASSWORD\";\n\n/*\n * Production CA pinning: replace setInsecure() with setCACert().\n * 1. Visit https://easyboard.live in a browser.\n * 2. Click the padlock → Certificate → Details → Export the Root CA\n *    (ISRG Root X1 for Let's Encrypt) as PEM.\n * 3. Paste the PEM content into a const char* here:\n *\n *    const char* rootCA = \\\n *      \"-----BEGIN CERTIFICATE-----\\n\"\n *      \"MIIFaz...\\n\"\n *      \"-----END CERTIFICATE-----\\n\";\n *\n * 4. Replace client.setInsecure() with client.setCACert(rootCA);\n */\n\nvoid setup() {\n  Serial.begin(115200);\n  WiFi.begin(ssid, password);\n  while (WiFi.status() != WL_CONNECTED) {\n    delay(500);\n    Serial.print(\".\");\n  }\n  Serial.println(\"\\nWiFi connected\");\n\n  WiFiClientSecure client;\n  client.setInsecure(); // For testing only — see CA pinning note above\n\n  HTTPClient http;\n  http.begin(client, \"https://easyboard.live/api/d/YOUR_DASHBOARD_ID/tiles\");\n  http.addHeader(\"Authorization\", \"Bearer YOUR_WRITE_TOKEN\");\n  http.addHeader(\"Content-Type\", \"application/json\");\n\n  String json = \"{\\\"updates\\\":[{\\\"target\\\":{\\\"id\\\":\\\"YOUR_TILE_ID\\\"},\\\"patch\\\":{\\\"value\\\":\\\"1500\\\"}}]}\";\n\n  int code = http.sendRequest(\"PATCH\", json);\n\n  if (code > 0) {\n    Serial.printf(\"HTTP %d\\n\", code);\n    Serial.println(http.getString());\n  } else {\n    Serial.printf(\"Request failed: %s\\n\", http.errorToString(code).c_str());\n  }\n\n  http.end();\n}\n\nvoid loop() {\n  // Runs once. Add delay + repeat here for periodic updates.\n}"}],"x-easyboardRecipes":[{"id":"set-tile-value","title":"Set a tile value","description":"Use an absolute string value to replace the existing tile value. Target by tile ID or alternateId.","samples":[{"lang":"curl","label":"curl","source":"curl -X PATCH https://easyboard.live/api/d/YOUR_DASHBOARD_ID/tiles \\\n  -H \"Authorization: Bearer YOUR_WRITE_TOKEN\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"updates\":[{\"target\":{\"id\":\"YOUR_TILE_ID\"},\"patch\":{\"value\":\"1500\"}}]}'"},{"lang":"c","label":"C","source":"/* Compile with: cc example.c -lcurl */\n#include <curl/curl.h>\n#include <stdio.h>\n\nstatic size_t write_callback(char *ptr, size_t size, size_t nmemb, void *userdata) {\n  size_t total = size * nmemb;\n  (void)userdata;\n  fwrite(ptr, 1, total, stdout);\n  return total;\n}\n\nint main(void) {\n  if (curl_global_init(CURL_GLOBAL_DEFAULT) != CURLE_OK) return 1;\n\n  CURL *curl = curl_easy_init();\n  if (!curl) {\n    curl_global_cleanup();\n    return 1;\n  }\n  struct curl_slist *headers = NULL;\n  const char *json = \"{\\\"updates\\\":[{\\\"target\\\":{\\\"id\\\":\\\"YOUR_TILE_ID\\\"},\\\"patch\\\":{\\\"value\\\":\\\"1500\\\"}}]}\";\n\n  curl_easy_setopt(curl, CURLOPT_URL, \"https://easyboard.live/api/d/YOUR_DASHBOARD_ID/tiles\");\n  curl_easy_setopt(curl, CURLOPT_CUSTOMREQUEST, \"PATCH\");\n  headers = curl_slist_append(headers, \"Authorization: Bearer YOUR_WRITE_TOKEN\");\n  headers = curl_slist_append(headers, \"Content-Type: application/json\");\n  curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers);\n  curl_easy_setopt(curl, CURLOPT_POSTFIELDS, json);\n  curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, write_callback);\n\n  CURLcode res = curl_easy_perform(curl);\n  if (res != CURLE_OK) {\n    fprintf(stderr, \"request failed: %s\\n\", curl_easy_strerror(res));\n  }\n\n  curl_slist_free_all(headers);\n  curl_easy_cleanup(curl);\n  curl_global_cleanup();\n  return res == CURLE_OK ? 0 : 1;\n}"},{"lang":"javascript","label":"JavaScript","source":"await fetch('https://easyboard.live/api/d/YOUR_DASHBOARD_ID/tiles', {\n  method: 'PATCH',\n  headers: {\n    Authorization: 'Bearer YOUR_WRITE_TOKEN',\n    'Content-Type': 'application/json',\n  },\n  body: JSON.stringify({\n    updates: [{ target: { id: 'YOUR_TILE_ID' }, patch: { value: '1500' } }],\n  }),\n});"},{"lang":"python","label":"Python","source":"import requests\n\nrequests.patch(\n    'https://easyboard.live/api/d/YOUR_DASHBOARD_ID/tiles',\n    headers={\n        'Authorization': 'Bearer YOUR_WRITE_TOKEN',\n        'Content-Type': 'application/json',\n    },\n    json={\n        'updates': [{'target': {'id': 'YOUR_TILE_ID'}, 'patch': {'value': '1500'}}],\n    },\n)"},{"lang":"powershell","label":"PowerShell","source":"Invoke-RestMethod https://easyboard.live/api/d/YOUR_DASHBOARD_ID/tiles `\n  -Method Patch `\n  -Headers @{ Authorization = 'Bearer YOUR_WRITE_TOKEN' } `\n  -ContentType 'application/json' `\n  -Body '{\"updates\":[{\"target\":{\"id\":\"YOUR_TILE_ID\"},\"patch\":{\"value\":\"1500\"}}]}'"},{"lang":"arduino","label":"Arduino (ESP32)","source":"/*\n * ESP32 Arduino — HTTPS request\n * Board: \"ESP32 Dev Module\" in Arduino IDE / PlatformIO\n * Library: ArduinoJson (install via Library Manager)\n *\n * HTTPS note: setInsecure() skips certificate verification.\n * For production, pin the root CA — see the comment below.\n */\n#include <WiFi.h>\n#include <WiFiClientSecure.h>\n#include <HTTPClient.h>\n\nconst char* ssid     = \"YOUR_WIFI_SSID\";\nconst char* password = \"YOUR_WIFI_PASSWORD\";\n\n/*\n * Production CA pinning: replace setInsecure() with setCACert().\n * 1. Visit https://easyboard.live in a browser.\n * 2. Click the padlock → Certificate → Details → Export the Root CA\n *    (ISRG Root X1 for Let's Encrypt) as PEM.\n * 3. Paste the PEM content into a const char* here:\n *\n *    const char* rootCA = \\\n *      \"-----BEGIN CERTIFICATE-----\\n\"\n *      \"MIIFaz...\\n\"\n *      \"-----END CERTIFICATE-----\\n\";\n *\n * 4. Replace client.setInsecure() with client.setCACert(rootCA);\n */\n\nvoid setup() {\n  Serial.begin(115200);\n  WiFi.begin(ssid, password);\n  while (WiFi.status() != WL_CONNECTED) {\n    delay(500);\n    Serial.print(\".\");\n  }\n  Serial.println(\"\\nWiFi connected\");\n\n  WiFiClientSecure client;\n  client.setInsecure(); // For testing only — see CA pinning note above\n\n  HTTPClient http;\n  http.begin(client, \"https://easyboard.live/api/d/YOUR_DASHBOARD_ID/tiles\");\n  http.addHeader(\"Authorization\", \"Bearer YOUR_WRITE_TOKEN\");\n  http.addHeader(\"Content-Type\", \"application/json\");\n\n  String json = \"{\\\"updates\\\":[{\\\"target\\\":{\\\"id\\\":\\\"YOUR_TILE_ID\\\"},\\\"patch\\\":{\\\"value\\\":\\\"1500\\\"}}]}\";\n\n  int code = http.sendRequest(\"PATCH\", json);\n\n  if (code > 0) {\n    Serial.printf(\"HTTP %d\\n\", code);\n    Serial.println(http.getString());\n  } else {\n    Serial.printf(\"Request failed: %s\\n\", http.errorToString(code).c_str());\n  }\n\n  http.end();\n}\n\nvoid loop() {\n  // Runs once. Add delay + repeat here for periodic updates.\n}"}]},{"id":"advance-toggle","title":"Advance a toggle tile to the next option","description":"Send `next: true` to cycle the toggle tile to its next configured option, wrapping after the last. This is server-side (like `delta` for counters) so rapid calls never skip an option.","samples":[{"lang":"curl","label":"curl","source":"curl -X PATCH https://easyboard.live/api/d/YOUR_DASHBOARD_ID/tiles \\\n  -H \"Authorization: Bearer YOUR_WRITE_TOKEN\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"updates\":[{\"target\":{\"id\":\"YOUR_TILE_ID\"},\"patch\":{\"next\":true}}]}'"},{"lang":"c","label":"C","source":"/* Compile with: cc example.c -lcurl */\n#include <curl/curl.h>\n#include <stdio.h>\n\nstatic size_t write_callback(char *ptr, size_t size, size_t nmemb, void *userdata) {\n  size_t total = size * nmemb;\n  (void)userdata;\n  fwrite(ptr, 1, total, stdout);\n  return total;\n}\n\nint main(void) {\n  if (curl_global_init(CURL_GLOBAL_DEFAULT) != CURLE_OK) return 1;\n\n  CURL *curl = curl_easy_init();\n  if (!curl) {\n    curl_global_cleanup();\n    return 1;\n  }\n  struct curl_slist *headers = NULL;\n  const char *json = \"{\\\"updates\\\":[{\\\"target\\\":{\\\"id\\\":\\\"YOUR_TILE_ID\\\"},\\\"patch\\\":{\\\"next\\\":true}}]}\";\n\n  curl_easy_setopt(curl, CURLOPT_URL, \"https://easyboard.live/api/d/YOUR_DASHBOARD_ID/tiles\");\n  curl_easy_setopt(curl, CURLOPT_CUSTOMREQUEST, \"PATCH\");\n  headers = curl_slist_append(headers, \"Authorization: Bearer YOUR_WRITE_TOKEN\");\n  headers = curl_slist_append(headers, \"Content-Type: application/json\");\n  curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers);\n  curl_easy_setopt(curl, CURLOPT_POSTFIELDS, json);\n  curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, write_callback);\n\n  CURLcode res = curl_easy_perform(curl);\n  if (res != CURLE_OK) {\n    fprintf(stderr, \"request failed: %s\\n\", curl_easy_strerror(res));\n  }\n\n  curl_slist_free_all(headers);\n  curl_easy_cleanup(curl);\n  curl_global_cleanup();\n  return res == CURLE_OK ? 0 : 1;\n}"},{"lang":"javascript","label":"JavaScript","source":"await fetch('https://easyboard.live/api/d/YOUR_DASHBOARD_ID/tiles', {\n  method: 'PATCH',\n  headers: {\n    Authorization: 'Bearer YOUR_WRITE_TOKEN',\n    'Content-Type': 'application/json',\n  },\n  body: JSON.stringify({\n    updates: [{ target: { id: 'YOUR_TILE_ID' }, patch: { next: true } }],\n  }),\n});"},{"lang":"python","label":"Python","source":"import requests\n\nrequests.patch(\n    'https://easyboard.live/api/d/YOUR_DASHBOARD_ID/tiles',\n    headers={\n        'Authorization': 'Bearer YOUR_WRITE_TOKEN',\n        'Content-Type': 'application/json',\n    },\n    json={\n        'updates': [{'target': {'id': 'YOUR_TILE_ID'}, 'patch': {'next': True}}],\n    },\n)"},{"lang":"powershell","label":"PowerShell","source":"Invoke-RestMethod https://easyboard.live/api/d/YOUR_DASHBOARD_ID/tiles `\n  -Method Patch `\n  -Headers @{ Authorization = 'Bearer YOUR_WRITE_TOKEN' } `\n  -ContentType 'application/json' `\n  -Body '{\"updates\":[{\"target\":{\"id\":\"YOUR_TILE_ID\"},\"patch\":{\"next\":true}}]}'"},{"lang":"arduino","label":"Arduino (ESP32)","source":"/*\n * ESP32 Arduino — HTTPS request\n * Board: \"ESP32 Dev Module\" in Arduino IDE / PlatformIO\n * Library: ArduinoJson (install via Library Manager)\n *\n * HTTPS note: setInsecure() skips certificate verification.\n * For production, pin the root CA — see the comment below.\n */\n#include <WiFi.h>\n#include <WiFiClientSecure.h>\n#include <HTTPClient.h>\n\nconst char* ssid     = \"YOUR_WIFI_SSID\";\nconst char* password = \"YOUR_WIFI_PASSWORD\";\n\n/*\n * Production CA pinning: replace setInsecure() with setCACert().\n * 1. Visit https://easyboard.live in a browser.\n * 2. Click the padlock → Certificate → Details → Export the Root CA\n *    (ISRG Root X1 for Let's Encrypt) as PEM.\n * 3. Paste the PEM content into a const char* here:\n *\n *    const char* rootCA = \\\n *      \"-----BEGIN CERTIFICATE-----\\n\"\n *      \"MIIFaz...\\n\"\n *      \"-----END CERTIFICATE-----\\n\";\n *\n * 4. Replace client.setInsecure() with client.setCACert(rootCA);\n */\n\nvoid setup() {\n  Serial.begin(115200);\n  WiFi.begin(ssid, password);\n  while (WiFi.status() != WL_CONNECTED) {\n    delay(500);\n    Serial.print(\".\");\n  }\n  Serial.println(\"\\nWiFi connected\");\n\n  WiFiClientSecure client;\n  client.setInsecure(); // For testing only — see CA pinning note above\n\n  HTTPClient http;\n  http.begin(client, \"https://easyboard.live/api/d/YOUR_DASHBOARD_ID/tiles\");\n  http.addHeader(\"Authorization\", \"Bearer YOUR_WRITE_TOKEN\");\n  http.addHeader(\"Content-Type\", \"application/json\");\n\n  String json = \"{\\\"updates\\\":[{\\\"target\\\":{\\\"id\\\":\\\"YOUR_TILE_ID\\\"},\\\"patch\\\":{\\\"next\\\":true}}]}\";\n\n  int code = http.sendRequest(\"PATCH\", json);\n\n  if (code > 0) {\n    Serial.printf(\"HTTP %d\\n\", code);\n    Serial.println(http.getString());\n  } else {\n    Serial.printf(\"Request failed: %s\\n\", http.errorToString(code).c_str());\n  }\n\n  http.end();\n}\n\nvoid loop() {\n  // Runs once. Add delay + repeat here for periodic updates.\n}"}]},{"id":"increment-counter","title":"Increment a counter with delta","description":"Send `delta` for counter-style updates. `delta` currently wins over `value` if both are present.","samples":[{"lang":"curl","label":"curl","source":"curl -X PATCH https://easyboard.live/api/d/YOUR_DASHBOARD_ID/tiles \\\n  -H \"Authorization: Bearer YOUR_WRITE_TOKEN\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"updates\":[{\"target\":{\"id\":\"YOUR_TILE_ID\"},\"patch\":{\"delta\":1}}]}'"},{"lang":"c","label":"C","source":"/* Compile with: cc example.c -lcurl */\n#include <curl/curl.h>\n#include <stdio.h>\n\nstatic size_t write_callback(char *ptr, size_t size, size_t nmemb, void *userdata) {\n  size_t total = size * nmemb;\n  (void)userdata;\n  fwrite(ptr, 1, total, stdout);\n  return total;\n}\n\nint main(void) {\n  if (curl_global_init(CURL_GLOBAL_DEFAULT) != CURLE_OK) return 1;\n\n  CURL *curl = curl_easy_init();\n  if (!curl) {\n    curl_global_cleanup();\n    return 1;\n  }\n  struct curl_slist *headers = NULL;\n  const char *json = \"{\\\"updates\\\":[{\\\"target\\\":{\\\"id\\\":\\\"YOUR_TILE_ID\\\"},\\\"patch\\\":{\\\"delta\\\":1}}]}\";\n\n  curl_easy_setopt(curl, CURLOPT_URL, \"https://easyboard.live/api/d/YOUR_DASHBOARD_ID/tiles\");\n  curl_easy_setopt(curl, CURLOPT_CUSTOMREQUEST, \"PATCH\");\n  headers = curl_slist_append(headers, \"Authorization: Bearer YOUR_WRITE_TOKEN\");\n  headers = curl_slist_append(headers, \"Content-Type: application/json\");\n  curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers);\n  curl_easy_setopt(curl, CURLOPT_POSTFIELDS, json);\n  curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, write_callback);\n\n  CURLcode res = curl_easy_perform(curl);\n  if (res != CURLE_OK) {\n    fprintf(stderr, \"request failed: %s\\n\", curl_easy_strerror(res));\n  }\n\n  curl_slist_free_all(headers);\n  curl_easy_cleanup(curl);\n  curl_global_cleanup();\n  return res == CURLE_OK ? 0 : 1;\n}"},{"lang":"javascript","label":"JavaScript","source":"await fetch('https://easyboard.live/api/d/YOUR_DASHBOARD_ID/tiles', {\n  method: 'PATCH',\n  headers: {\n    Authorization: 'Bearer YOUR_WRITE_TOKEN',\n    'Content-Type': 'application/json',\n  },\n  body: JSON.stringify({\n    updates: [{ target: { id: 'YOUR_TILE_ID' }, patch: { delta: 1 } }],\n  }),\n});"},{"lang":"python","label":"Python","source":"import requests\n\nrequests.patch(\n    'https://easyboard.live/api/d/YOUR_DASHBOARD_ID/tiles',\n    headers={\n        'Authorization': 'Bearer YOUR_WRITE_TOKEN',\n        'Content-Type': 'application/json',\n    },\n    json={\n        'updates': [{'target': {'id': 'YOUR_TILE_ID'}, 'patch': {'delta': 1}}],\n    },\n)"},{"lang":"powershell","label":"PowerShell","source":"Invoke-RestMethod https://easyboard.live/api/d/YOUR_DASHBOARD_ID/tiles `\n  -Method Patch `\n  -Headers @{ Authorization = 'Bearer YOUR_WRITE_TOKEN' } `\n  -ContentType 'application/json' `\n  -Body '{\"updates\":[{\"target\":{\"id\":\"YOUR_TILE_ID\"},\"patch\":{\"delta\":1}}]}'"},{"lang":"arduino","label":"Arduino (ESP32)","source":"/*\n * ESP32 Arduino — HTTPS request\n * Board: \"ESP32 Dev Module\" in Arduino IDE / PlatformIO\n * Library: ArduinoJson (install via Library Manager)\n *\n * HTTPS note: setInsecure() skips certificate verification.\n * For production, pin the root CA — see the comment below.\n */\n#include <WiFi.h>\n#include <WiFiClientSecure.h>\n#include <HTTPClient.h>\n\nconst char* ssid     = \"YOUR_WIFI_SSID\";\nconst char* password = \"YOUR_WIFI_PASSWORD\";\n\n/*\n * Production CA pinning: replace setInsecure() with setCACert().\n * 1. Visit https://easyboard.live in a browser.\n * 2. Click the padlock → Certificate → Details → Export the Root CA\n *    (ISRG Root X1 for Let's Encrypt) as PEM.\n * 3. Paste the PEM content into a const char* here:\n *\n *    const char* rootCA = \\\n *      \"-----BEGIN CERTIFICATE-----\\n\"\n *      \"MIIFaz...\\n\"\n *      \"-----END CERTIFICATE-----\\n\";\n *\n * 4. Replace client.setInsecure() with client.setCACert(rootCA);\n */\n\nvoid setup() {\n  Serial.begin(115200);\n  WiFi.begin(ssid, password);\n  while (WiFi.status() != WL_CONNECTED) {\n    delay(500);\n    Serial.print(\".\");\n  }\n  Serial.println(\"\\nWiFi connected\");\n\n  WiFiClientSecure client;\n  client.setInsecure(); // For testing only — see CA pinning note above\n\n  HTTPClient http;\n  http.begin(client, \"https://easyboard.live/api/d/YOUR_DASHBOARD_ID/tiles\");\n  http.addHeader(\"Authorization\", \"Bearer YOUR_WRITE_TOKEN\");\n  http.addHeader(\"Content-Type\", \"application/json\");\n\n  String json = \"{\\\"updates\\\":[{\\\"target\\\":{\\\"id\\\":\\\"YOUR_TILE_ID\\\"},\\\"patch\\\":{\\\"delta\\\":1}}]}\";\n\n  int code = http.sendRequest(\"PATCH\", json);\n\n  if (code > 0) {\n    Serial.printf(\"HTTP %d\\n\", code);\n    Serial.println(http.getString());\n  } else {\n    Serial.printf(\"Request failed: %s\\n\", http.errorToString(code).c_str());\n  }\n\n  http.end();\n}\n\nvoid loop() {\n  // Runs once. Add delay + repeat here for periodic updates.\n}"}]},{"id":"set-text","title":"Set a text tile","description":"Text tiles use the same batch PATCH endpoint with a string value.","samples":[{"lang":"curl","label":"curl","source":"curl -X PATCH https://easyboard.live/api/d/YOUR_DASHBOARD_ID/tiles \\\n  -H \"Authorization: Bearer YOUR_WRITE_TOKEN\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"updates\":[{\"target\":{\"id\":\"YOUR_TILE_ID\"},\"patch\":{\"value\":\"Kitchen closes at 4pm\"}}]}'"},{"lang":"c","label":"C","source":"/* Compile with: cc example.c -lcurl */\n#include <curl/curl.h>\n#include <stdio.h>\n\nstatic size_t write_callback(char *ptr, size_t size, size_t nmemb, void *userdata) {\n  size_t total = size * nmemb;\n  (void)userdata;\n  fwrite(ptr, 1, total, stdout);\n  return total;\n}\n\nint main(void) {\n  if (curl_global_init(CURL_GLOBAL_DEFAULT) != CURLE_OK) return 1;\n\n  CURL *curl = curl_easy_init();\n  if (!curl) {\n    curl_global_cleanup();\n    return 1;\n  }\n  struct curl_slist *headers = NULL;\n  const char *json = \"{\\\"updates\\\":[{\\\"target\\\":{\\\"id\\\":\\\"YOUR_TILE_ID\\\"},\\\"patch\\\":{\\\"value\\\":\\\"Kitchen closes at 4pm\\\"}}]}\";\n\n  curl_easy_setopt(curl, CURLOPT_URL, \"https://easyboard.live/api/d/YOUR_DASHBOARD_ID/tiles\");\n  curl_easy_setopt(curl, CURLOPT_CUSTOMREQUEST, \"PATCH\");\n  headers = curl_slist_append(headers, \"Authorization: Bearer YOUR_WRITE_TOKEN\");\n  headers = curl_slist_append(headers, \"Content-Type: application/json\");\n  curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers);\n  curl_easy_setopt(curl, CURLOPT_POSTFIELDS, json);\n  curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, write_callback);\n\n  CURLcode res = curl_easy_perform(curl);\n  if (res != CURLE_OK) {\n    fprintf(stderr, \"request failed: %s\\n\", curl_easy_strerror(res));\n  }\n\n  curl_slist_free_all(headers);\n  curl_easy_cleanup(curl);\n  curl_global_cleanup();\n  return res == CURLE_OK ? 0 : 1;\n}"},{"lang":"javascript","label":"JavaScript","source":"await fetch('https://easyboard.live/api/d/YOUR_DASHBOARD_ID/tiles', {\n  method: 'PATCH',\n  headers: {\n    Authorization: 'Bearer YOUR_WRITE_TOKEN',\n    'Content-Type': 'application/json',\n  },\n  body: JSON.stringify({\n    updates: [{ target: { id: 'YOUR_TILE_ID' }, patch: { value: 'Kitchen closes at 4pm' } }],\n  }),\n});"},{"lang":"python","label":"Python","source":"import requests\n\nrequests.patch(\n    'https://easyboard.live/api/d/YOUR_DASHBOARD_ID/tiles',\n    headers={\n        'Authorization': 'Bearer YOUR_WRITE_TOKEN',\n        'Content-Type': 'application/json',\n    },\n    json={\n        'updates': [{'target': {'id': 'YOUR_TILE_ID'}, 'patch': {'value': 'Kitchen closes at 4pm'}}],\n    },\n)"},{"lang":"powershell","label":"PowerShell","source":"Invoke-RestMethod https://easyboard.live/api/d/YOUR_DASHBOARD_ID/tiles `\n  -Method Patch `\n  -Headers @{ Authorization = 'Bearer YOUR_WRITE_TOKEN' } `\n  -ContentType 'application/json' `\n  -Body '{\"updates\":[{\"target\":{\"id\":\"YOUR_TILE_ID\"},\"patch\":{\"value\":\"Kitchen closes at 4pm\"}}]}'"},{"lang":"arduino","label":"Arduino (ESP32)","source":"/*\n * ESP32 Arduino — HTTPS request\n * Board: \"ESP32 Dev Module\" in Arduino IDE / PlatformIO\n * Library: ArduinoJson (install via Library Manager)\n *\n * HTTPS note: setInsecure() skips certificate verification.\n * For production, pin the root CA — see the comment below.\n */\n#include <WiFi.h>\n#include <WiFiClientSecure.h>\n#include <HTTPClient.h>\n\nconst char* ssid     = \"YOUR_WIFI_SSID\";\nconst char* password = \"YOUR_WIFI_PASSWORD\";\n\n/*\n * Production CA pinning: replace setInsecure() with setCACert().\n * 1. Visit https://easyboard.live in a browser.\n * 2. Click the padlock → Certificate → Details → Export the Root CA\n *    (ISRG Root X1 for Let's Encrypt) as PEM.\n * 3. Paste the PEM content into a const char* here:\n *\n *    const char* rootCA = \\\n *      \"-----BEGIN CERTIFICATE-----\\n\"\n *      \"MIIFaz...\\n\"\n *      \"-----END CERTIFICATE-----\\n\";\n *\n * 4. Replace client.setInsecure() with client.setCACert(rootCA);\n */\n\nvoid setup() {\n  Serial.begin(115200);\n  WiFi.begin(ssid, password);\n  while (WiFi.status() != WL_CONNECTED) {\n    delay(500);\n    Serial.print(\".\");\n  }\n  Serial.println(\"\\nWiFi connected\");\n\n  WiFiClientSecure client;\n  client.setInsecure(); // For testing only — see CA pinning note above\n\n  HTTPClient http;\n  http.begin(client, \"https://easyboard.live/api/d/YOUR_DASHBOARD_ID/tiles\");\n  http.addHeader(\"Authorization\", \"Bearer YOUR_WRITE_TOKEN\");\n  http.addHeader(\"Content-Type\", \"application/json\");\n\n  String json = \"{\\\"updates\\\":[{\\\"target\\\":{\\\"id\\\":\\\"YOUR_TILE_ID\\\"},\\\"patch\\\":{\\\"value\\\":\\\"Kitchen closes at 4pm\\\"}}]}\";\n\n  int code = http.sendRequest(\"PATCH\", json);\n\n  if (code > 0) {\n    Serial.printf(\"HTTP %d\\n\", code);\n    Serial.println(http.getString());\n  } else {\n    Serial.printf(\"Request failed: %s\\n\", http.errorToString(code).c_str());\n  }\n\n  http.end();\n}\n\nvoid loop() {\n  // Runs once. Add delay + repeat here for periodic updates.\n}"}]},{"id":"set-progress","title":"Set a progress tile","description":"Progress values are stored exactly as sent; the UI clamps the visual bar client-side.","samples":[{"lang":"curl","label":"curl","source":"curl -X PATCH https://easyboard.live/api/d/YOUR_DASHBOARD_ID/tiles \\\n  -H \"Authorization: Bearer YOUR_WRITE_TOKEN\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"updates\":[{\"target\":{\"id\":\"YOUR_TILE_ID\"},\"patch\":{\"value\":\"75\"}}]}'"},{"lang":"c","label":"C","source":"/* Compile with: cc example.c -lcurl */\n#include <curl/curl.h>\n#include <stdio.h>\n\nstatic size_t write_callback(char *ptr, size_t size, size_t nmemb, void *userdata) {\n  size_t total = size * nmemb;\n  (void)userdata;\n  fwrite(ptr, 1, total, stdout);\n  return total;\n}\n\nint main(void) {\n  if (curl_global_init(CURL_GLOBAL_DEFAULT) != CURLE_OK) return 1;\n\n  CURL *curl = curl_easy_init();\n  if (!curl) {\n    curl_global_cleanup();\n    return 1;\n  }\n  struct curl_slist *headers = NULL;\n  const char *json = \"{\\\"updates\\\":[{\\\"target\\\":{\\\"id\\\":\\\"YOUR_TILE_ID\\\"},\\\"patch\\\":{\\\"value\\\":\\\"75\\\"}}]}\";\n\n  curl_easy_setopt(curl, CURLOPT_URL, \"https://easyboard.live/api/d/YOUR_DASHBOARD_ID/tiles\");\n  curl_easy_setopt(curl, CURLOPT_CUSTOMREQUEST, \"PATCH\");\n  headers = curl_slist_append(headers, \"Authorization: Bearer YOUR_WRITE_TOKEN\");\n  headers = curl_slist_append(headers, \"Content-Type: application/json\");\n  curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers);\n  curl_easy_setopt(curl, CURLOPT_POSTFIELDS, json);\n  curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, write_callback);\n\n  CURLcode res = curl_easy_perform(curl);\n  if (res != CURLE_OK) {\n    fprintf(stderr, \"request failed: %s\\n\", curl_easy_strerror(res));\n  }\n\n  curl_slist_free_all(headers);\n  curl_easy_cleanup(curl);\n  curl_global_cleanup();\n  return res == CURLE_OK ? 0 : 1;\n}"},{"lang":"javascript","label":"JavaScript","source":"await fetch('https://easyboard.live/api/d/YOUR_DASHBOARD_ID/tiles', {\n  method: 'PATCH',\n  headers: {\n    Authorization: 'Bearer YOUR_WRITE_TOKEN',\n    'Content-Type': 'application/json',\n  },\n  body: JSON.stringify({\n    updates: [{ target: { id: 'YOUR_TILE_ID' }, patch: { value: '75' } }],\n  }),\n});"},{"lang":"python","label":"Python","source":"import requests\n\nrequests.patch(\n    'https://easyboard.live/api/d/YOUR_DASHBOARD_ID/tiles',\n    headers={\n        'Authorization': 'Bearer YOUR_WRITE_TOKEN',\n        'Content-Type': 'application/json',\n    },\n    json={\n        'updates': [{'target': {'id': 'YOUR_TILE_ID'}, 'patch': {'value': '75'}}],\n    },\n)"},{"lang":"powershell","label":"PowerShell","source":"Invoke-RestMethod https://easyboard.live/api/d/YOUR_DASHBOARD_ID/tiles `\n  -Method Patch `\n  -Headers @{ Authorization = 'Bearer YOUR_WRITE_TOKEN' } `\n  -ContentType 'application/json' `\n  -Body '{\"updates\":[{\"target\":{\"id\":\"YOUR_TILE_ID\"},\"patch\":{\"value\":\"75\"}}]}'"},{"lang":"arduino","label":"Arduino (ESP32)","source":"/*\n * ESP32 Arduino — HTTPS request\n * Board: \"ESP32 Dev Module\" in Arduino IDE / PlatformIO\n * Library: ArduinoJson (install via Library Manager)\n *\n * HTTPS note: setInsecure() skips certificate verification.\n * For production, pin the root CA — see the comment below.\n */\n#include <WiFi.h>\n#include <WiFiClientSecure.h>\n#include <HTTPClient.h>\n\nconst char* ssid     = \"YOUR_WIFI_SSID\";\nconst char* password = \"YOUR_WIFI_PASSWORD\";\n\n/*\n * Production CA pinning: replace setInsecure() with setCACert().\n * 1. Visit https://easyboard.live in a browser.\n * 2. Click the padlock → Certificate → Details → Export the Root CA\n *    (ISRG Root X1 for Let's Encrypt) as PEM.\n * 3. Paste the PEM content into a const char* here:\n *\n *    const char* rootCA = \\\n *      \"-----BEGIN CERTIFICATE-----\\n\"\n *      \"MIIFaz...\\n\"\n *      \"-----END CERTIFICATE-----\\n\";\n *\n * 4. Replace client.setInsecure() with client.setCACert(rootCA);\n */\n\nvoid setup() {\n  Serial.begin(115200);\n  WiFi.begin(ssid, password);\n  while (WiFi.status() != WL_CONNECTED) {\n    delay(500);\n    Serial.print(\".\");\n  }\n  Serial.println(\"\\nWiFi connected\");\n\n  WiFiClientSecure client;\n  client.setInsecure(); // For testing only — see CA pinning note above\n\n  HTTPClient http;\n  http.begin(client, \"https://easyboard.live/api/d/YOUR_DASHBOARD_ID/tiles\");\n  http.addHeader(\"Authorization\", \"Bearer YOUR_WRITE_TOKEN\");\n  http.addHeader(\"Content-Type\", \"application/json\");\n\n  String json = \"{\\\"updates\\\":[{\\\"target\\\":{\\\"id\\\":\\\"YOUR_TILE_ID\\\"},\\\"patch\\\":{\\\"value\\\":\\\"75\\\"}}]}\";\n\n  int code = http.sendRequest(\"PATCH\", json);\n\n  if (code > 0) {\n    Serial.printf(\"HTTP %d\\n\", code);\n    Serial.println(http.getString());\n  } else {\n    Serial.printf(\"Request failed: %s\\n\", http.errorToString(code).c_str());\n  }\n\n  http.end();\n}\n\nvoid loop() {\n  // Runs once. Add delay + repeat here for periodic updates.\n}"}]}]}},"/api/d/{dashboardId}/tiles/{tileRef}":{"get":{"operationId":"getTile","summary":"Get a single tile","description":"Fetch one tile by its generated tile ID or, when `lookup=alternateId`, by its alternateId.","tags":["Tiles"],"parameters":[{"name":"dashboardId","in":"path","required":true,"description":"The 12-character dashboard ID from your dashboard URL.","schema":{"type":"string","example":"abc123xyz456"}},{"name":"tileRef","in":"path","required":true,"description":"A tile ID, or an alternateId when used with `lookup=alternateId`.","schema":{"type":"string","example":"tile-abc123"}},{"name":"lookup","in":"query","required":false,"description":"How to interpret the `tileRef` path segment. Defaults to `id`. Use `alternateId` to fetch or delete by alternateId.","schema":{"type":"string","enum":["id","alternateId"],"default":"id","example":"alternateId"}}],"responses":{"200":{"description":"Tile found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Tile"},"example":{"id":"tile-abc123","alternateId":"RevenueTile","type":"metric","title":"Revenue","value":"1250","unit":"$","size":"medium","order":0}}}},"400":{"description":"Invalid lookup mode.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"},"example":{"error":"lookup must be either \"id\" or \"alternateId\""}}}},"404":{"description":"Dashboard or tile not found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"},"example":{"error":"Tile not found"}}}}}},"delete":{"operationId":"deleteTile","summary":"Delete a tile","description":"Permanently removes a tile and re-indexes the remaining `order` fields. By default `tileRef` is treated as a tile ID; use `lookup=alternateId` to delete by alternateId.","tags":["Tiles"],"parameters":[{"name":"dashboardId","in":"path","required":true,"description":"The 12-character dashboard ID from your dashboard URL.","schema":{"type":"string","example":"abc123xyz456"}},{"name":"tileRef","in":"path","required":true,"description":"A tile ID, or an alternateId when used with `lookup=alternateId`.","schema":{"type":"string","example":"tile-abc123"}},{"name":"lookup","in":"query","required":false,"description":"How to interpret the `tileRef` path segment. Defaults to `id`. Use `alternateId` to fetch or delete by alternateId.","schema":{"type":"string","enum":["id","alternateId"],"default":"id","example":"alternateId"}}],"security":[{"WriteTokenAuth":[]}],"responses":{"204":{"description":"Tile deleted successfully."},"400":{"description":"Invalid lookup mode.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"},"example":{"error":"lookup must be either \"id\" or \"alternateId\""}}}},"401":{"description":"Missing or incorrect write token.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"},"example":{"error":"Unauthorized"}}}},"404":{"description":"Dashboard or tile not found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"},"example":{"error":"Tile not found"}}}}},"x-codeSamples":[{"lang":"curl","label":"curl","source":"curl -X DELETE https://easyboard.live/api/d/YOUR_DASHBOARD_ID/tiles/YOUR_TILE_ID \\\n  -H \"Authorization: Bearer YOUR_WRITE_TOKEN\""},{"lang":"c","label":"C","source":"/* Compile with: cc example.c -lcurl */\n#include <curl/curl.h>\n#include <stdio.h>\n\nstatic size_t write_callback(char *ptr, size_t size, size_t nmemb, void *userdata) {\n  size_t total = size * nmemb;\n  (void)userdata;\n  fwrite(ptr, 1, total, stdout);\n  return total;\n}\n\nint main(void) {\n  if (curl_global_init(CURL_GLOBAL_DEFAULT) != CURLE_OK) return 1;\n\n  CURL *curl = curl_easy_init();\n  if (!curl) {\n    curl_global_cleanup();\n    return 1;\n  }\n  struct curl_slist *headers = NULL;\n\n  curl_easy_setopt(curl, CURLOPT_URL, \"https://easyboard.live/api/d/YOUR_DASHBOARD_ID/tiles/YOUR_TILE_ID\");\n  curl_easy_setopt(curl, CURLOPT_CUSTOMREQUEST, \"DELETE\");\n  headers = curl_slist_append(headers, \"Authorization: Bearer YOUR_WRITE_TOKEN\");\n  curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers);\n  curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, write_callback);\n\n  CURLcode res = curl_easy_perform(curl);\n  if (res != CURLE_OK) {\n    fprintf(stderr, \"request failed: %s\\n\", curl_easy_strerror(res));\n  }\n\n  curl_slist_free_all(headers);\n  curl_easy_cleanup(curl);\n  curl_global_cleanup();\n  return res == CURLE_OK ? 0 : 1;\n}"},{"lang":"javascript","label":"JavaScript","source":"await fetch(\n  'https://easyboard.live/api/d/YOUR_DASHBOARD_ID/tiles/YOUR_TILE_ID',\n  {\n    method: 'DELETE',\n    headers: { Authorization: 'Bearer YOUR_WRITE_TOKEN' },\n  }\n);"},{"lang":"python","label":"Python","source":"import requests\n\nrequests.delete(\n    'https://easyboard.live/api/d/YOUR_DASHBOARD_ID/tiles/YOUR_TILE_ID',\n    headers={'Authorization': 'Bearer YOUR_WRITE_TOKEN'},\n)"},{"lang":"powershell","label":"PowerShell","source":"Invoke-RestMethod https://easyboard.live/api/d/YOUR_DASHBOARD_ID/tiles/YOUR_TILE_ID `\n  -Method Delete `\n  -Headers @{ Authorization = 'Bearer YOUR_WRITE_TOKEN' }"},{"lang":"arduino","label":"Arduino (ESP32)","source":"/*\n * ESP32 Arduino — HTTPS request\n * Board: \"ESP32 Dev Module\" in Arduino IDE / PlatformIO\n *\n * HTTPS note: setInsecure() skips certificate verification.\n * For production, pin the root CA — see the comment below.\n */\n#include <WiFi.h>\n#include <WiFiClientSecure.h>\n#include <HTTPClient.h>\n\nconst char* ssid     = \"YOUR_WIFI_SSID\";\nconst char* password = \"YOUR_WIFI_PASSWORD\";\n\n/*\n * Production CA pinning: replace setInsecure() with setCACert().\n * 1. Visit https://easyboard.live in a browser.\n * 2. Click the padlock → Certificate → Details → Export the Root CA\n *    (ISRG Root X1 for Let's Encrypt) as PEM.\n * 3. Paste the PEM content into a const char* here:\n *\n *    const char* rootCA = \\\n *      \"-----BEGIN CERTIFICATE-----\\n\"\n *      \"MIIFaz...\\n\"\n *      \"-----END CERTIFICATE-----\\n\";\n *\n * 4. Replace client.setInsecure() with client.setCACert(rootCA);\n */\n\nvoid setup() {\n  Serial.begin(115200);\n  WiFi.begin(ssid, password);\n  while (WiFi.status() != WL_CONNECTED) {\n    delay(500);\n    Serial.print(\".\");\n  }\n  Serial.println(\"\\nWiFi connected\");\n\n  WiFiClientSecure client;\n  client.setInsecure(); // For testing only — see CA pinning note above\n\n  HTTPClient http;\n  http.begin(client, \"https://easyboard.live/api/d/YOUR_DASHBOARD_ID/tiles/YOUR_TILE_ID\");\n  http.addHeader(\"Authorization\", \"Bearer YOUR_WRITE_TOKEN\");\n\n  int code = http.sendRequest(\"DELETE\");\n\n  if (code > 0) {\n    Serial.printf(\"HTTP %d\\n\", code);\n    Serial.println(http.getString());\n  } else {\n    Serial.printf(\"Request failed: %s\\n\", http.errorToString(code).c_str());\n  }\n\n  http.end();\n}\n\nvoid loop() {\n  // Runs once. Add delay + repeat here for periodic updates.\n}"}]}},"/api/d/{dashboardId}/tiles/reorder":{"post":{"operationId":"reorderTiles","summary":"Reorder tiles","description":"Updates all tile order values at once. Used by drag-and-drop and bulk reordering.","tags":["Tiles"],"parameters":[{"name":"dashboardId","in":"path","required":true,"description":"The 12-character dashboard ID from your dashboard URL.","schema":{"type":"string","example":"abc123xyz456"}}],"security":[{"WriteTokenAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ReorderRequest"},"example":{"ids":["tile-def456","tile-abc123"]}}}},"responses":{"204":{"description":"Tiles reordered successfully."},"400":{"description":"Invalid JSON body or ids must be an array.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"},"example":{"error":"ids must be an array"}}}},"401":{"description":"Missing or incorrect write token.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"},"example":{"error":"Unauthorized"}}}},"404":{"description":"Dashboard not found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"},"example":{"error":"Dashboard not found"}}}}},"x-codeSamples":[{"lang":"curl","label":"curl","source":"curl -X POST https://easyboard.live/api/d/YOUR_DASHBOARD_ID/tiles/reorder \\\n  -H \"Authorization: Bearer YOUR_WRITE_TOKEN\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"ids\":[\"tile-def456\",\"tile-abc123\"]}'"},{"lang":"c","label":"C","source":"/* Compile with: cc example.c -lcurl */\n#include <curl/curl.h>\n#include <stdio.h>\n\nstatic size_t write_callback(char *ptr, size_t size, size_t nmemb, void *userdata) {\n  size_t total = size * nmemb;\n  (void)userdata;\n  fwrite(ptr, 1, total, stdout);\n  return total;\n}\n\nint main(void) {\n  if (curl_global_init(CURL_GLOBAL_DEFAULT) != CURLE_OK) return 1;\n\n  CURL *curl = curl_easy_init();\n  if (!curl) {\n    curl_global_cleanup();\n    return 1;\n  }\n  struct curl_slist *headers = NULL;\n  const char *json = \"{\\\"ids\\\":[\\\"tile-def456\\\",\\\"tile-abc123\\\"]}\";\n\n  curl_easy_setopt(curl, CURLOPT_URL, \"https://easyboard.live/api/d/YOUR_DASHBOARD_ID/tiles/reorder\");\n  curl_easy_setopt(curl, CURLOPT_CUSTOMREQUEST, \"POST\");\n  headers = curl_slist_append(headers, \"Authorization: Bearer YOUR_WRITE_TOKEN\");\n  headers = curl_slist_append(headers, \"Content-Type: application/json\");\n  curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers);\n  curl_easy_setopt(curl, CURLOPT_POSTFIELDS, json);\n  curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, write_callback);\n\n  CURLcode res = curl_easy_perform(curl);\n  if (res != CURLE_OK) {\n    fprintf(stderr, \"request failed: %s\\n\", curl_easy_strerror(res));\n  }\n\n  curl_slist_free_all(headers);\n  curl_easy_cleanup(curl);\n  curl_global_cleanup();\n  return res == CURLE_OK ? 0 : 1;\n}"},{"lang":"javascript","label":"JavaScript","source":"await fetch(\n  'https://easyboard.live/api/d/YOUR_DASHBOARD_ID/tiles/reorder',\n  {\n    method: 'POST',\n    headers: {\n      Authorization: 'Bearer YOUR_WRITE_TOKEN',\n      'Content-Type': 'application/json',\n    },\n    body: JSON.stringify({ ids: ['tile-def456', 'tile-abc123'] }),\n  }\n);"},{"lang":"python","label":"Python","source":"import requests\n\nrequests.post(\n    'https://easyboard.live/api/d/YOUR_DASHBOARD_ID/tiles/reorder',\n    headers={\n        'Authorization': 'Bearer YOUR_WRITE_TOKEN',\n        'Content-Type': 'application/json',\n    },\n    json={'ids': ['tile-def456', 'tile-abc123']},\n)"},{"lang":"powershell","label":"PowerShell","source":"Invoke-RestMethod https://easyboard.live/api/d/YOUR_DASHBOARD_ID/tiles/reorder `\n  -Method Post `\n  -Headers @{ Authorization = 'Bearer YOUR_WRITE_TOKEN' } `\n  -ContentType 'application/json' `\n  -Body '{\"ids\":[\"tile-def456\",\"tile-abc123\"]}'"},{"lang":"arduino","label":"Arduino (ESP32)","source":"/*\n * ESP32 Arduino — HTTPS request\n * Board: \"ESP32 Dev Module\" in Arduino IDE / PlatformIO\n * Library: ArduinoJson (install via Library Manager)\n *\n * HTTPS note: setInsecure() skips certificate verification.\n * For production, pin the root CA — see the comment below.\n */\n#include <WiFi.h>\n#include <WiFiClientSecure.h>\n#include <HTTPClient.h>\n\nconst char* ssid     = \"YOUR_WIFI_SSID\";\nconst char* password = \"YOUR_WIFI_PASSWORD\";\n\n/*\n * Production CA pinning: replace setInsecure() with setCACert().\n * 1. Visit https://easyboard.live in a browser.\n * 2. Click the padlock → Certificate → Details → Export the Root CA\n *    (ISRG Root X1 for Let's Encrypt) as PEM.\n * 3. Paste the PEM content into a const char* here:\n *\n *    const char* rootCA = \\\n *      \"-----BEGIN CERTIFICATE-----\\n\"\n *      \"MIIFaz...\\n\"\n *      \"-----END CERTIFICATE-----\\n\";\n *\n * 4. Replace client.setInsecure() with client.setCACert(rootCA);\n */\n\nvoid setup() {\n  Serial.begin(115200);\n  WiFi.begin(ssid, password);\n  while (WiFi.status() != WL_CONNECTED) {\n    delay(500);\n    Serial.print(\".\");\n  }\n  Serial.println(\"\\nWiFi connected\");\n\n  WiFiClientSecure client;\n  client.setInsecure(); // For testing only — see CA pinning note above\n\n  HTTPClient http;\n  http.begin(client, \"https://easyboard.live/api/d/YOUR_DASHBOARD_ID/tiles/reorder\");\n  http.addHeader(\"Authorization\", \"Bearer YOUR_WRITE_TOKEN\");\n  http.addHeader(\"Content-Type\", \"application/json\");\n\n  String json = \"{\\\"ids\\\":[\\\"tile-def456\\\",\\\"tile-abc123\\\"]}\";\n\n  int code = http.POST(json);\n\n  if (code > 0) {\n    Serial.printf(\"HTTP %d\\n\", code);\n    Serial.println(http.getString());\n  } else {\n    Serial.printf(\"Request failed: %s\\n\", http.errorToString(code).c_str());\n  }\n\n  http.end();\n}\n\nvoid loop() {\n  // Runs once. Add delay + repeat here for periodic updates.\n}"}]}},"/api/d/{dashboardId}/stream":{"get":{"operationId":"streamDashboard","summary":"Stream live dashboard snapshots","description":"Opens a Server-Sent Events stream for one dashboard. The first event is the current dashboard snapshot, later events are updated snapshots. If the dashboard is removed elsewhere, the stream may emit a `deleted` event before closing.","tags":["Realtime"],"parameters":[{"name":"dashboardId","in":"path","required":true,"description":"The 12-character dashboard ID from your dashboard URL.","schema":{"type":"string","example":"abc123xyz456"}}],"responses":{"200":{"description":"Server-Sent Events stream.","content":{"text/event-stream":{"schema":{"type":"string"},"example":"data: {\"name\":\"Coffee Shop Sales\",\"tiles\":[{\"id\":\"tile-abc123\",\"type\":\"metric\",\"title\":\"Revenue\",\"value\":\"1250\",\"unit\":\"$\",\"size\":\"medium\",\"order\":0}]}\n\n: keepalive\n\nevent: deleted\ndata: {}\n\n"}}},"404":{"description":"Dashboard not found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"},"example":{"error":"Dashboard not found"}}}}},"x-codeSamples":[{"lang":"curl","label":"curl","source":"curl -N https://easyboard.live/api/d/YOUR_DASHBOARD_ID/stream"},{"lang":"c","label":"C","source":"/* Compile with: cc example.c -lcurl */\n#include <curl/curl.h>\n#include <stdio.h>\n\nstatic size_t stream_callback(char *ptr, size_t size, size_t nmemb, void *userdata) {\n  size_t total = size * nmemb;\n  (void)userdata;\n  fwrite(ptr, 1, total, stdout);\n  fflush(stdout);\n  return total;\n}\n\nint main(void) {\n  if (curl_global_init(CURL_GLOBAL_DEFAULT) != CURLE_OK) return 1;\n\n  CURL *curl = curl_easy_init();\n  if (!curl) {\n    curl_global_cleanup();\n    return 1;\n  }\n  curl_easy_setopt(curl, CURLOPT_URL, \"https://easyboard.live/api/d/YOUR_DASHBOARD_ID/stream\");\n  curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, stream_callback);\n\n  CURLcode res = curl_easy_perform(curl);\n  if (res != CURLE_OK) {\n    fprintf(stderr, \"stream failed: %s\\n\", curl_easy_strerror(res));\n  }\n\n  curl_easy_cleanup(curl);\n  curl_global_cleanup();\n  return res == CURLE_OK ? 0 : 1;\n}"},{"lang":"javascript","label":"JavaScript","source":"const stream = new EventSource(\n  'https://easyboard.live/api/d/YOUR_DASHBOARD_ID/stream'\n);\n\nstream.onmessage = (event) => {\n  const snapshot = JSON.parse(event.data);\n  console.log(snapshot.tiles);\n};\n\nstream.addEventListener('deleted', () => {\n  console.log('Dashboard deleted');\n  stream.close();\n});"},{"lang":"python","label":"Python","source":"import requests\n\nwith requests.get(\n    'https://easyboard.live/api/d/YOUR_DASHBOARD_ID/stream',\n    stream=True,\n) as res:\n    for line in res.iter_lines(decode_unicode=True):\n        if line:\n            print(line)"},{"lang":"powershell","label":"PowerShell","source":"# Invoke-RestMethod does not support SSE streaming.\n# Use curl.exe (built into Windows 10+) instead:\ncurl.exe -N https://easyboard.live/api/d/YOUR_DASHBOARD_ID/stream"},{"lang":"arduino","label":"Arduino (ESP32)","source":"/*\n * ESP32 Arduino — SSE stream listener\n * Board: \"ESP32 Dev Module\" in Arduino IDE / PlatformIO\n *\n * HTTPS note: setInsecure() skips certificate verification.\n * For production, pin the root CA — see the comment below.\n */\n#include <WiFi.h>\n#include <WiFiClientSecure.h>\n\nconst char* ssid     = \"YOUR_WIFI_SSID\";\nconst char* password = \"YOUR_WIFI_PASSWORD\";\n\n/*\n * Production CA pinning: replace setInsecure() with setCACert().\n * 1. Visit https://easyboard.live in a browser.\n * 2. Click the padlock → Certificate → Details → Export the Root CA\n *    (ISRG Root X1 for Let's Encrypt) as PEM.\n * 3. Paste the PEM content into a const char* here:\n *\n *    const char* rootCA = \\\n *      \"-----BEGIN CERTIFICATE-----\\n\"\n *      \"MIIFaz...\\n\"\n *      \"-----END CERTIFICATE-----\\n\";\n *\n * 4. Replace client.setInsecure() with client.setCACert(rootCA);\n */\n\nWiFiClientSecure client;\n\nvoid setup() {\n  Serial.begin(115200);\n  WiFi.begin(ssid, password);\n  while (WiFi.status() != WL_CONNECTED) {\n    delay(500);\n    Serial.print(\".\");\n  }\n  Serial.println(\"\\nWiFi connected\");\n\n  client.setInsecure(); // For testing only — see CA pinning note above\n\n  if (!client.connect(\"easyboard.live\", 443)) {\n    Serial.println(\"Connection failed\");\n    return;\n  }\n\n  client.println(\"GET /api/d/YOUR_DASHBOARD_ID/stream HTTP/1.1\");\n  client.println(\"Host: easyboard.live\");\n  client.println(\"Accept: text/event-stream\");\n  client.println();\n}\n\nvoid loop() {\n  if (client.connected() && client.available()) {\n    String line = client.readStringUntil('\\n');\n    if (line.startsWith(\"data: \")) {\n      Serial.println(line.substring(6));\n    }\n  }\n\n  if (!client.connected()) {\n    Serial.println(\"Disconnected — restarting in 5 s\");\n    delay(5000);\n    ESP.restart();\n  }\n}"}],"x-easyboardRecipes":[{"id":"subscribe-sse","title":"Subscribe with SSE","description":"Use a streaming connection when you want live dashboard snapshots without polling.","samples":[{"lang":"curl","label":"curl","source":"curl -N https://easyboard.live/api/d/YOUR_DASHBOARD_ID/stream"},{"lang":"c","label":"C","source":"/* Compile with: cc example.c -lcurl */\n#include <curl/curl.h>\n#include <stdio.h>\n\nstatic size_t stream_callback(char *ptr, size_t size, size_t nmemb, void *userdata) {\n  size_t total = size * nmemb;\n  (void)userdata;\n  fwrite(ptr, 1, total, stdout);\n  fflush(stdout);\n  return total;\n}\n\nint main(void) {\n  if (curl_global_init(CURL_GLOBAL_DEFAULT) != CURLE_OK) return 1;\n\n  CURL *curl = curl_easy_init();\n  if (!curl) {\n    curl_global_cleanup();\n    return 1;\n  }\n  curl_easy_setopt(curl, CURLOPT_URL, \"https://easyboard.live/api/d/YOUR_DASHBOARD_ID/stream\");\n  curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, stream_callback);\n\n  CURLcode res = curl_easy_perform(curl);\n  if (res != CURLE_OK) {\n    fprintf(stderr, \"stream failed: %s\\n\", curl_easy_strerror(res));\n  }\n\n  curl_easy_cleanup(curl);\n  curl_global_cleanup();\n  return res == CURLE_OK ? 0 : 1;\n}"},{"lang":"javascript","label":"JavaScript","source":"const stream = new EventSource(\n  'https://easyboard.live/api/d/YOUR_DASHBOARD_ID/stream'\n);\n\nstream.onmessage = (event) => {\n  const snapshot = JSON.parse(event.data);\n  console.log(snapshot.tiles);\n};\n\nstream.addEventListener('deleted', () => {\n  stream.close();\n});"},{"lang":"python","label":"Python","source":"import requests\n\nwith requests.get(\n    'https://easyboard.live/api/d/YOUR_DASHBOARD_ID/stream',\n    stream=True,\n) as res:\n    for line in res.iter_lines(decode_unicode=True):\n        if line:\n            print(line)"},{"lang":"powershell","label":"PowerShell","source":"# Invoke-RestMethod does not support SSE streaming.\n# Use curl.exe (built into Windows 10+) instead:\ncurl.exe -N https://easyboard.live/api/d/YOUR_DASHBOARD_ID/stream"},{"lang":"arduino","label":"Arduino (ESP32)","source":"/*\n * ESP32 Arduino — SSE stream listener\n * Board: \"ESP32 Dev Module\" in Arduino IDE / PlatformIO\n *\n * HTTPS note: setInsecure() skips certificate verification.\n * For production, pin the root CA — see the comment below.\n */\n#include <WiFi.h>\n#include <WiFiClientSecure.h>\n\nconst char* ssid     = \"YOUR_WIFI_SSID\";\nconst char* password = \"YOUR_WIFI_PASSWORD\";\n\n/*\n * Production CA pinning: replace setInsecure() with setCACert().\n * 1. Visit https://easyboard.live in a browser.\n * 2. Click the padlock → Certificate → Details → Export the Root CA\n *    (ISRG Root X1 for Let's Encrypt) as PEM.\n * 3. Paste the PEM content into a const char* here:\n *\n *    const char* rootCA = \\\n *      \"-----BEGIN CERTIFICATE-----\\n\"\n *      \"MIIFaz...\\n\"\n *      \"-----END CERTIFICATE-----\\n\";\n *\n * 4. Replace client.setInsecure() with client.setCACert(rootCA);\n */\n\nWiFiClientSecure client;\n\nvoid setup() {\n  Serial.begin(115200);\n  WiFi.begin(ssid, password);\n  while (WiFi.status() != WL_CONNECTED) {\n    delay(500);\n    Serial.print(\".\");\n  }\n  Serial.println(\"\\nWiFi connected\");\n\n  client.setInsecure(); // For testing only — see CA pinning note above\n\n  if (!client.connect(\"easyboard.live\", 443)) {\n    Serial.println(\"Connection failed\");\n    return;\n  }\n\n  client.println(\"GET /api/d/YOUR_DASHBOARD_ID/stream HTTP/1.1\");\n  client.println(\"Host: easyboard.live\");\n  client.println(\"Accept: text/event-stream\");\n  client.println();\n}\n\nvoid loop() {\n  if (client.connected() && client.available()) {\n    String line = client.readStringUntil('\\n');\n    if (line.startsWith(\"data: \")) {\n      Serial.println(line.substring(6));\n    }\n  }\n\n  if (!client.connected()) {\n    Serial.println(\"Disconnected — restarting in 5 s\");\n    delay(5000);\n    ESP.restart();\n  }\n}"}]}]}}}}