REST API v1 / IoT Category

Smart Dashboard Integration API

Part of our comprehensive IoT platform - Complete documentation for device registration, real‑time state sync, command queue, and heartbeat reconciliation. Works with any HTTP-capable platform including Python applications, mobile apps, web frontends, and hardware devices. Includes ESP8266/ESP32 reference firmware implementation.

IoT Category Device Management ESP8266/ESP32 Ready
Base URL: https://odivora.com/api/v1/smart-dashboard
All endpoints require API key & secret headers (except public device heartbeats with signed requests).
Try it now Back to Tools

Authentication

Every control, device listing, and command request must include the following headers:

X-API-Key: YOUR_API_KEY
X-API-Secret: YOUR_API_SECRET
Heartbeat and reconcile endpoints also use these headers to validate the board identity.

Getting Your API Keys

  1. Navigate to the Tools Dashboard
  2. Click on "API Keys" in the navigation menu
  3. Generate a new API key and secret pair
  4. Copy and securely store your credentials (they won't be shown again)
  5. Use the keys in your API requests as shown in the examples above

Security Tip: Never expose your API keys in client-side code or public repositories. Use environment variables or secure configuration management.

Quick Start

Get up and running in minutes with these ready-to-use examples:

List Your Devices

# List all devices
curl -X GET "https://odivora.com/api/v1/smart-dashboard/devices" \
  -H "X-API-Key: YOUR_API_KEY" \
  -H "X-API-Secret: YOUR_API_SECRET"

# Add a new device
curl -X POST "https://odivora.com/api/v1/smart-dashboard/devices" \
  -H "Content-Type: application/json" \
  -H "X-API-Key: YOUR_API_KEY" \
  -H "X-API-Secret: YOUR_API_SECRET" \
  -d '{
    "name": "Living Room Sensor",
    "type": "temperature",
    "location": "living_room"
  }'

Get Started Now

Use Tool (No Code)

Launch the Smart Dashboard interface and manage your IoT devices visually. No coding required.

Launch Dashboard

Try API Now

Test the Smart Dashboard API directly from your browser with our interactive console.

Device management

POST /devices

Register a new smart device (switch/plug/lock). Maximum 5 devices per user.

Request body (JSON):
{
  "name": "Living Room Bulb",
  "device_type": "smart_switch",
  "room": "Living Room",
  "wifi_board": "ESP8266_NODEMCU_01",
  "config": {}
}
Response (201):
{
  "success": true,
  "device": {
    "device_id": "SD-A42EACCBB767",
    "name": "Living Room Bulb",
    "status": "offline",
    "config": {}
  },
  "message": "Device added successfully"
}
GET /devices

Retrieve all devices owned by authenticated user (including shared devices).

{
  "success": true,
  "devices": [
    {
      "device_id": "SD-64316F62789F",
      "name": "Kitchen Plug",
      "status": "online",
      "config": { "power": true }
    }
  ]
}
DELETE /devices/{device_id}

Permanently delete a device and all associated shares & command queue. Owner only.

{ "success": true, "message": "Device deleted permanently" }

API Response Formats

All API responses follow a consistent structure for easy integration and error handling.

Success Response

{
  "success": true,
  "data": {
    // Response data varies by endpoint
    "devices": [...],
    "device_id": "abc123",
    "status": "active"
  },
  "message": "Operation completed successfully",
  "timestamp": "2026-05-05T12:00:00Z"
}

Error Response

{
  "success": false,
  "error": {
    "code": "DEVICE_NOT_FOUND",
    "message": "Device with ID 'abc123' not found",
    "details": {
      "device_id": "abc123",
      "user_id": "user_456"
    }
  },
  "timestamp": "2026-05-05T12:00:00Z"
}

Common Error Codes

HTTP Status Error Code Description
400 INVALID_REQUEST Request body is malformed or missing required fields
401 UNAUTHORIZED Invalid or missing API credentials
403 FORBIDDEN Insufficient permissions to access this resource
404 DEVICE_NOT_FOUND Requested device does not exist
429 RATE_LIMIT_EXCEEDED Too many requests, please try again later
500 INTERNAL_ERROR Server error, please contact support

API Versioning

Our API uses semantic versioning to ensure backward compatibility and smooth upgrades.

Current Version: v1

All endpoints use the /api/v1 base URL prefix:

https://odivora.com/api/v1/smart-dashboard/{endpoint}

Versioning Policy:

  • Major versions (v2, v3): Breaking changes that require code updates
  • Minor versions (v1.1, v1.2): New features, backward compatible
  • Patch versions (v1.1.1): Bug fixes, no API changes

Version Support:

v1.x - Currently supported and actively maintained

v0.x - Deprecated, will be removed in future releases

Device control & state sync

POST /device/{device_id}/control

Send a command to a physical device (on/off/toggle/lock/unlock). The command is queued if device offline.

{
  "command": "on",
  "parameters": {}
}
Response:
{
  "success": true,
  "message": "Command on executed",
  "device_state": "on"
}
GET /device/{device_id}/status?wifi_board_id=ESP8266

Firmware polls this endpoint to fetch pending commands.

{
  "command": "on",
  "timestamp": 1705920000
}

Heartbeat & state reconciliation

These endpoints guarantee that the server always knows the real physical state of each device, even after network drops.

GET /device/{device_id}/heartbeat

Called periodically (every 4s) by NodeMCU. Reports online status and current power state. Includes automatic retry on failure.

Query parameters:
paramdescription
statusonline or offline
wifi_board_idboard identifier (ESP8266)
initial_stateURL-encoded JSON {"power":true/false}
Example NodeMCU call (from code above):
String url = "/api/v1/smart_dashboard/device/SD-A42EACCBB767/heartbeat?status=online&wifi_board_id=ESP8266&initial_state=%7B%22power%22%3Atrue%7D";
POST /device/{device_id}/confirm_state

After executing a command, firmware confirms new state so server tracks it reliably.

{
  "power": true,
  "timestamp": 1705920450,
  "wifi_board_id": "ESP8266"
}
POST /device/{device_id}/reconcile

Used on boot / WiFi reconnection. Server responds with any missed commands while device was offline.

{
  "pending_commands": 1,
  "commands_sent": 1,
  "command": "off"
}

Reference Implementation

Platform-Agnostic API

The Smart Dashboard API is hardware-agnostic and works with any device capable of making HTTP requests. While this documentation includes a NodeMCU (ESP8266/ESP32) firmware example as a reference implementation, the API can also be integrated with:

  • 🐍 Python applications on Raspberry Pi, Linux servers, or desktop
  • 📱 Mobile applications (iOS/Android) with native HTTP clients
  • 🌐 Web frontends using JavaScript fetch() or Axios
  • 🔧 Other WiFi-enabled devices (Arduino with WiFi shields, ESP32, etc.)

Note: The provided firmware is only a reference implementation, not a limitation of supported platforms.

NodeMCU (ESP8266/ESP32) Reference Firmware

Complete production‑grade firmware example with auto‑reconnect, heartbeat state reporting, and delivery‑confirmed command processing.


/*
 * ================================================================
 * NodeMCU Smart Bulb Controller - 4 Devices
 * ================================================================
 *
 * ESP8266 / NodeMCU
 *
 * Features:
 *   - 4 independently controlled relay outputs
 *   - HTTPS communication with ODIVORA Smart Dashboard API
 *   - Persistent relay state using LittleFS
 *   - Device heartbeat
 *   - Remote ON / OFF / TOGGLE commands
 *   - State confirmation
 *   - Device reconciliation after boot
 *   - Automatic WiFi reconnection
 *   - HTTP/JSON error handling
 *   - Request timeouts
 *
 * ODIVORA
 * Smart Dashboard Device Controller
 * ================================================================
 */

#include <ESP8266WiFi.h>
#include <ESP8266HTTPClient.h>
#include <WiFiClientSecure.h>
#include <LittleFS.h>
#include <ArduinoJson.h>

// ================================================================
// WIFI CONFIGURATION
// ================================================================

const char* WIFI_SSID     = "YOUR_WIFI_SSID";
const char* WIFI_PASSWORD = "YOUR_WIFI_PASSWORD";

// ================================================================
// SERVER CONFIGURATION
// ================================================================

const char* SERVER_URL = "https://odivora.com";

const char* WIFI_BOARD_ID = "ESP8266";

const char* API_KEY ="YOUR_API_KEY";

const char* API_SECRET ="YOUR_API_SECRET";

// ================================================================
// DEVICE CONFIGURATION
// ================================================================

#define DEVICE_COUNT 4

const char* DEVICE_IDS[DEVICE_COUNT] = {
  "SD-A42EACCBB767",
  "SD-64316F62789F",
  "SD-5D22DC343C9B",
  "SD-E2C60A2E5EDD"
};

// Relay pins
const int RELAY_PINS[DEVICE_COUNT] = {
  D1,
  D2,
  D5,
  D6
};

// Set to true if your relay module activates when pin is LOW.
const bool RELAY_ACTIVE_LOW = false;

// ================================================================
// ONBOARD LED CONFIGURATION
// ================================================================

// NodeMCU onboard LED (D4/GPIO2) - active LOW (ON when pin is LOW)
const int LED_PIN = LED_BUILTIN;
const bool LED_ACTIVE_LOW = true;

// Blink timing
const unsigned long BLINK_ON_MS  = 100;  // LED on duration
const unsigned long BLINK_OFF_MS = 100;  // LED off duration
const int BLINK_COUNT = 3;               // Number of blinks per command

// ================================================================
// TIMING
// ================================================================

const unsigned long HEARTBEAT_INTERVAL = 4000;  // 4 seconds - well within 30s server timeout
const unsigned long COMMAND_INTERVAL    = 750;
const unsigned long WIFI_RETRY_INTERVAL = 5000;
const unsigned long HEARTBEAT_RETRY_INTERVAL = 2000;  // Retry failed heartbeats after 2s

const unsigned long HTTP_TIMEOUT = 6000;

// ================================================================
// PERSISTENT STATE
// ================================================================

const char* STATE_FILE = "/relay_state.json";

// ================================================================
// RUNTIME STATE
// ================================================================

bool bulbState[DEVICE_COUNT] = {
  false,
  false,
  false,
  false
};

String lastCommand[DEVICE_COUNT];

/*
 * Per-device heartbeat timers plus a single round-robin cursor.
 *
 * Command checks are batched: ONE request returns pending commands for every
 * relay, so a command is picked up on the next 750ms poll no matter how many
 * devices the board drives. loop() runs, in priority order, the batch command
 * poll then the round-robin device's heartbeat, spreading blocking HTTPS/TLS
 * requests so one sluggish request cannot stall every device (the ESP8266
 * performs one blocking request at a time).
 */
unsigned long lastHeartbeat[DEVICE_COUNT] = { 0, 0, 0, 0 };
unsigned long lastCmdCheck = 0;
unsigned long lastHeartbeatRetry = 0;
unsigned long lastWiFiRetry = 0;
int pollIndex = 0;
bool heartbeatFailed[DEVICE_COUNT] = { false, false, false, false };

// ================================================================
// HTTP CLIENTS
// ================================================================

WiFiClientSecure secureClient;
WiFiClient plainClient;

bool useTLS = false;


// ================================================================
// FORWARD DECLARATIONS
// ================================================================

void connectWiFi();
void reconnectWiFi();

void setRelay(int idx, bool on);
void blinkLED(int count);
void executeCommand(int idx, const String& cmd);

void heartbeatDevice(int i);
void retryHeartbeatDevice(int i);
void checkDeviceCommandsBatch();

void confirmState(int idx);

void reconcileAllDevices();

bool loadPersistentState();
bool savePersistentState();

bool beginHttp(HTTPClient& http, const String& path);

String urlEncode(const String& value);

void printWiFiStatus();
void printDeviceStates();


// ================================================================
// SETUP
// ================================================================

void setup() {

  Serial.begin(115200);
  delay(200);

  Serial.println();
  Serial.println();
  Serial.println("================================================");
  Serial.println(" ODIVORA NODEMCU SMART BULB CONTROLLER");
  Serial.println("================================================");

  // --------------------------------------------------------------
  // Determine HTTP / HTTPS mode
  // --------------------------------------------------------------

  useTLS = String(SERVER_URL).startsWith("https://");

  Serial.print("Server: ");
  Serial.println(SERVER_URL);

  if (useTLS) {

    Serial.println("HTTPS mode enabled.");

    /*
     * This disables certificate verification.
     *
     * It is convenient for ESP8266 deployments because the device
     * does not need a hard-coded certificate.
     *
     * For maximum security, replace this with setCACert().
     */
    secureClient.setInsecure();

    secureClient.setTimeout(HTTP_TIMEOUT / 1000);

  } else {

    Serial.println("HTTP mode enabled.");
    plainClient.setTimeout(HTTP_TIMEOUT / 1000);
  }

  // --------------------------------------------------------------
  // Initialize LittleFS
  // --------------------------------------------------------------

  Serial.println();
  Serial.println("Initializing LittleFS...");

  if (!LittleFS.begin()) {

    Serial.println("ERROR: LittleFS initialization failed.");

    /*
     * Try formatting the filesystem.
     *
     * WARNING:
     * This can erase existing LittleFS data.
     */
    Serial.println("Attempting LittleFS format...");

    if (!LittleFS.format()) {

      Serial.println("ERROR: LittleFS format failed.");

    } else {

      if (LittleFS.begin()) {
        Serial.println("LittleFS initialized after format.");
      } else {
        Serial.println("ERROR: LittleFS still unavailable.");
      }
    }

  } else {

    Serial.println("LittleFS OK.");
  }

  // --------------------------------------------------------------
  // Load saved relay states
  // --------------------------------------------------------------

  Serial.println();
  Serial.println("Loading persistent relay states...");

  if (!loadPersistentState()) {

    Serial.println(
      "No valid saved state found. "
      "Using all relays OFF."
    );

    for (int i = 0; i < DEVICE_COUNT; i++) {
      bulbState[i] = false;
    }

  } else {

    Serial.println("Saved relay states restored.");
  }

  // --------------------------------------------------------------
  // Configure relay pins
  // --------------------------------------------------------------

  Serial.println();
  Serial.println("Initializing relay outputs...");

  for (int i = 0; i < DEVICE_COUNT; i++) {

    pinMode(RELAY_PINS[i], OUTPUT);

    // Restore saved state.
    setRelay(i, bulbState[i]);

    Serial.print("Device ");
    Serial.print(i + 1);

    Serial.print(" [");
    Serial.print(DEVICE_IDS[i]);
    Serial.print("] -> ");

    Serial.println(
      bulbState[i] ? "ON" : "OFF"
    );
  }

  // --------------------------------------------------------------
  // Configure onboard LED
  // --------------------------------------------------------------

  Serial.println();
  Serial.println("Initializing onboard LED...");

  pinMode(LED_PIN, OUTPUT);
  digitalWrite(LED_PIN, LED_ACTIVE_LOW ? HIGH : LOW);  // LED off initially
  Serial.println("Onboard LED ready.");

  // --------------------------------------------------------------
  // Connect WiFi
  // --------------------------------------------------------------

  Serial.println();
  connectWiFi();

  // --------------------------------------------------------------
  // Synchronize local state with server
  // --------------------------------------------------------------

  if (WiFi.status() == WL_CONNECTED) {

    Serial.println();
    Serial.println("Reconciling devices with server...");

    reconcileAllDevices();

  } else {

    Serial.println(
      "WiFi unavailable. "
      "Server reconciliation skipped."
    );
  }

  Serial.println();
  Serial.println("================================================");
  Serial.println(" CONTROLLER READY");
  Serial.println("================================================");

  printDeviceStates();
}


// ================================================================
// MAIN LOOP
// ================================================================

void loop() {

  // --------------------------------------------------------------
  // WiFi monitoring
  // --------------------------------------------------------------

  if (WiFi.status() != WL_CONNECTED) {

    reconnectWiFi();

    /*
     * Do not attempt API requests while offline.
     */
    delay(10);
    return;
  }

  unsigned long now = millis();

  // --------------------------------------------------------------
  // Round-robin cursor selects which device gets its heartbeat pass
  // --------------------------------------------------------------

  int i = pollIndex;
  pollIndex = (pollIndex + 1) % DEVICE_COUNT;

  // 1) Batch command poll has top priority: commands for EVERY relay are
  //    fetched in one request, so a heartbeat can never delay command pickup.
  if (now - lastCmdCheck >= COMMAND_INTERVAL) {

    lastCmdCheck = now;

    checkDeviceCommandsBatch();
  }

  // 2) Heartbeat for the same device when its interval has elapsed. The
  //    timestamp captured before the command poll keeps a stable cadence even
  //    when that device just processed a command.
  if (now - lastHeartbeat[i] >= HEARTBEAT_INTERVAL) {

    lastHeartbeat[i] = now;

    heartbeatDevice(i);
  }

  // 3) Retry a failed heartbeat, at most one device per pass.
  if (heartbeatFailed[i] &&
      now - lastHeartbeatRetry >= HEARTBEAT_RETRY_INTERVAL) {

    lastHeartbeatRetry = now;

    retryHeartbeatDevice(i);
  }

  yield();
}


// ================================================================
// HTTP BEGIN
// ================================================================

bool beginHttp(HTTPClient& http, const String& path) {

  String url = String(SERVER_URL) + path;

  Serial.print("HTTP URL: ");
  Serial.println(url);

  http.setTimeout(HTTP_TIMEOUT);

  bool result = false;

  if (useTLS) {

    result = http.begin(
      secureClient,
      url
    );

  } else {

    result = http.begin(
      plainClient,
      url
    );
  }

  if (!result) {

    Serial.println(
      "ERROR: HTTPClient.begin() failed."
    );

    return false;
  }

  return true;
}


// ================================================================
// HEARTBEAT
// ================================================================

void heartbeatDevice(int i) {

  if (WiFi.status() != WL_CONNECTED) {
    return;
  }

  HTTPClient http;

  /*
   * Construct state JSON.
   *
   * Example:
   * {"power":true,"timestamp":123456}
   */
  DynamicJsonDocument stateDoc(256);

  stateDoc["power"] = bulbState[i];
  stateDoc["timestamp"] = millis();

  String stateJson;

  serializeJson(
    stateDoc,
    stateJson
  );

  String path =
    "/api/v1/smart_dashboard/device/" +
    String(DEVICE_IDS[i]) +
    "/heartbeat?status=online" +
    "&wifi_board_id=" +
    urlEncode(WIFI_BOARD_ID) +
    "&initial_state=" +
    urlEncode(stateJson);

  if (!beginHttp(http, path)) {
    http.end();
    heartbeatFailed[i] = true;
    return;
  }

  /*
   * Include authentication.
   */
  http.addHeader(
    "X-API-Key",
    API_KEY
  );

  http.addHeader(
    "X-API-Secret",
    API_SECRET
  );

  http.addHeader(
    "Accept",
    "application/json"
  );

  int code = http.GET();

  Serial.print("Heartbeat ");
  Serial.print(i + 1);
  Serial.print(" -> HTTP ");
  Serial.println(code);

  if (code < 0) {

    Serial.print(
      "Heartbeat error: "
    );

    Serial.println(
      http.errorToString(code)
    );

    heartbeatFailed[i] = true;

  } else if (code >= 200 && code < 300) {

    Serial.println(
      "Heartbeat accepted."
    );

    heartbeatFailed[i] = false;

  } else {

    Serial.print(
      "Heartbeat rejected. Response: "
    );

    Serial.println(
      http.getString()
    );

    heartbeatFailed[i] = true;
  }

  http.end();
}


// ================================================================
// HEARTBEAT RETRY
// ================================================================

void retryHeartbeatDevice(int i) {

  if (!heartbeatFailed[i]) {
    return;
  }

  if (WiFi.status() != WL_CONNECTED) {
    return;
  }

  HTTPClient http;

  DynamicJsonDocument stateDoc(256);

  stateDoc["power"] = bulbState[i];
  stateDoc["timestamp"] = millis();

  String stateJson;

  serializeJson(
    stateDoc,
    stateJson
  );

  String path =
    "/api/v1/smart_dashboard/device/" +
    String(DEVICE_IDS[i]) +
    "/heartbeat?status=online" +
    "&wifi_board_id=" +
    urlEncode(WIFI_BOARD_ID) +
    "&initial_state=" +
    urlEncode(stateJson);

  if (!beginHttp(http, path)) {
    http.end();
    return;
  }

  http.addHeader(
    "X-API-Key",
    API_KEY
  );

  http.addHeader(
    "X-API-Secret",
    API_SECRET
  );

  http.addHeader(
    "Accept",
    "application/json"
  );

  int code = http.GET();

  Serial.print("Retry heartbeat ");
  Serial.print(i + 1);
  Serial.print(" -> HTTP ");
  Serial.println(code);

  if (code >= 200 && code < 300) {
    Serial.println("Retry heartbeat accepted.");
    heartbeatFailed[i] = false;
  }

  http.end();
}


// ================================================================
// COMMAND CHECK
// ================================================================

void checkDeviceCommandsBatch() {

  if (WiFi.status() != WL_CONNECTED) {
    return;
  }

  HTTPClient http;

    // One request covers every relay, so command pickup is bounded by a single
    // recurring 750ms poll instead of the old round-robin (up to 4 x 1.5s).

    String deviceIds = String(DEVICE_IDS[0]);
    for (int k = 1; k < DEVICE_COUNT; k++) {
      deviceIds += ",";
      deviceIds += DEVICE_IDS[k];
    }

    String path =
      "/api/v1/smart_dashboard/devices/status?wifi_board_id=" +
      urlEncode(WIFI_BOARD_ID) +
      "&device_ids=" +
      urlEncode(deviceIds);

    if (!beginHttp(http, path)) {
      http.end();
      return;
    }

    http.addHeader(
      "X-API-Key",
      API_KEY
    );

    http.addHeader(
      "X-API-Secret",
      API_SECRET
    );

    http.addHeader(
      "Accept",
      "application/json"
    );

    int code = http.GET();

    Serial.print("Batch status -> HTTP ");
    Serial.println(code);

    if (code == HTTP_CODE_OK) {

      String response = http.getString();

      Serial.print("Response: ");
      Serial.println(response);

      DynamicJsonDocument doc(2048);

      DeserializationError error =
        deserializeJson(
          doc,
          response
        );

      if (error) {

        Serial.print(
          "JSON parse error: "
        );

        Serial.println(
          error.c_str()
        );

      } else {

        JsonArray devices =
          doc["devices"].as<JsonArray>();

        if (!devices.isNull()) {

          for (JsonObject entry : devices) {

            // Map each returned entry to its relay index by device id; the
            // server may order the entries arbitrarily.
            String deviceId =
              entry["device_id"].as<String>();

            int idx = -1;
            for (int k = 0; k < DEVICE_COUNT; k++) {
              if (deviceId == DEVICE_IDS[k]) {
                idx = k;
                break;
              }
            }

            if (idx < 0) {
              continue;
            }

            if (entry.containsKey("command") &&
                !entry["command"].isNull()) {

              String cmd =
                entry["command"].as<String>();

              cmd.trim();
              cmd.toLowerCase();

              Serial.print("Device ");
              Serial.print(idx + 1);
              Serial.print(" Command: ");
              Serial.println(cmd);

              /*
               * Only execute a command if it is different
               * from the previously processed command.
               */
              if (cmd.length() > 0 &&
                  cmd != lastCommand[idx]) {

                Serial.print(
                  "New command detected for device "
                );

                Serial.println(idx + 1);

                lastCommand[idx] = cmd;

                executeCommand(
                  idx,
                  cmd
                );

              } else if (cmd.length() > 0 &&
                         cmd == lastCommand[idx]) {

                /*
                 * Duplicate command - already executed once. Re-confirm the
                 * current state so the server can mark the pending command
                 * complete instead of leaving it queued for the full command
                 * timeout (which made commands appear as never executed).
                 */
                Serial.println(
                  "Duplicate command - re-confirming state"
                );

                confirmState(idx);
              }
            }
          }
        }
      }

    } else if (code < 0) {

      Serial.print(
        "Status request error: "
      );

      Serial.println(
        http.errorToString(code)
      );

    } else {

      Serial.print(
        "Status request rejected: "
      );

      Serial.println(
        http.getString()
      );
    }

    http.end();
}


// ================================================================
// EXECUTE COMMAND
// ================================================================

void executeCommand(
  int idx,
  const String& cmd
) {

  if (idx < 0 || idx >= DEVICE_COUNT) {
    return;
  }

  Serial.println();
  Serial.print("Executing command for device ");
  Serial.print(idx + 1);
  Serial.print(": ");
  Serial.println(cmd);

  bool newState;

  // --------------------------------------------------------------
  // ON
  // --------------------------------------------------------------

  if (cmd == "on") {

    newState = true;

  }

  // --------------------------------------------------------------
  // OFF
  // --------------------------------------------------------------

  else if (cmd == "off") {

    newState = false;

  }

  // --------------------------------------------------------------
  // TOGGLE
  // --------------------------------------------------------------

  else if (cmd == "toggle") {

    newState = !bulbState[idx];

  }

  // --------------------------------------------------------------
  // Unknown command
  // --------------------------------------------------------------

  else {

    Serial.print(
      "Unknown command: "
    );

    Serial.println(cmd);

    return;
  }

  // --------------------------------------------------------------
  // Apply state
  // --------------------------------------------------------------

  bulbState[idx] = newState;

  setRelay(
    idx,
    newState
  );

  // --------------------------------------------------------------
  // Blink LED to indicate command execution
  // --------------------------------------------------------------

  blinkLED(BLINK_COUNT);

  // --------------------------------------------------------------
  // Persist state
  // --------------------------------------------------------------

  if (!savePersistentState()) {

    Serial.println(
      "WARNING: Failed to save relay state."
    );
  }

  // --------------------------------------------------------------
  // Confirm with server
  // --------------------------------------------------------------

  confirmState(idx);

  Serial.print("Device ");
  Serial.print(idx + 1);
  Serial.print(" is now ");
  Serial.println(
    newState ? "ON" : "OFF"
  );
}


// ================================================================
// SET RELAY
// ================================================================

void setRelay(
  int idx,
  bool on
) {

  if (idx < 0 || idx >= DEVICE_COUNT) {
    return;
  }

  int outputState;

  if (RELAY_ACTIVE_LOW) {

    outputState =
      on ? LOW : HIGH;

  } else {

    outputState =
      on ? HIGH : LOW;
  }

  digitalWrite(
    RELAY_PINS[idx],
    outputState
  );
}


// ================================================================
// BLINK LED
// ================================================================

void blinkLED(int count) {

  for (int i = 0; i < count; i++) {

    // Turn LED ON
    digitalWrite(LED_PIN, LED_ACTIVE_LOW ? LOW : HIGH);
    delay(BLINK_ON_MS);

    // Turn LED OFF
    digitalWrite(LED_PIN, LED_ACTIVE_LOW ? HIGH : LOW);

    // Don't delay after the last blink
    if (i < count - 1) {
      delay(BLINK_OFF_MS);
    }
  }
}


// ================================================================
// CONFIRM STATE
// ================================================================

void confirmState(int idx) {

  if (WiFi.status() != WL_CONNECTED) {

    Serial.println(
      "Cannot confirm state: WiFi offline."
    );

    return;
  }

  HTTPClient http;

  String path =
    "/api/v1/smart_dashboard/device/" +
    String(DEVICE_IDS[idx]) +
    "/confirm_state";

  if (!beginHttp(http, path)) {
    http.end();
    return;
  }

  DynamicJsonDocument doc(256);

  doc["power"] = bulbState[idx];
  doc["wifi_board_id"] = WIFI_BOARD_ID;

  String payload;

  serializeJson(
    doc,
    payload
  );

  http.addHeader(
    "Content-Type",
    "application/json"
  );

  http.addHeader(
    "Accept",
    "application/json"
  );

  http.addHeader(
    "X-API-Key",
    API_KEY
  );

  http.addHeader(
    "X-API-Secret",
    API_SECRET
  );

  Serial.print(
    "Confirming device "
  );

  Serial.print(idx + 1);
  Serial.print(": ");
  Serial.println(payload);

  int code =
    http.POST(payload);

  Serial.print(
    "Confirm response: HTTP "
  );

  Serial.println(code);

  if (code < 0) {

    Serial.print(
      "Confirm error: "
    );

    Serial.println(
      http.errorToString(code)
    );

  } else if (code >= 200 && code < 300) {

    Serial.println(
      "State confirmed successfully."
    );

  } else {

    Serial.print(
      "State confirmation failed: "
    );

    Serial.println(
      http.getString()
    );
  }

  http.end();
}


// ================================================================
// RECONCILE DEVICES
// ================================================================

void reconcileAllDevices() {

  for (int i = 0; i < DEVICE_COUNT; i++) {

    if (WiFi.status() != WL_CONNECTED) {
      return;
    }

    HTTPClient http;

    String path =
      "/api/v1/smart_dashboard/device/" +
      String(DEVICE_IDS[i]) +
      "/reconcile";

    if (!beginHttp(http, path)) {
      http.end();
      continue;
    }

    DynamicJsonDocument doc(256);

    doc["power"] =
      bulbState[i];

    doc["wifi_board_id"] =
      WIFI_BOARD_ID;

    String payload;

    serializeJson(
      doc,
      payload
    );

    http.addHeader(
      "Content-Type",
      "application/json"
    );

    http.addHeader(
      "Accept",
      "application/json"
    );

    http.addHeader(
      "X-API-Key",
      API_KEY
    );

    http.addHeader(
      "X-API-Secret",
      API_SECRET
    );

    Serial.print(
      "Reconciling device "
    );

    Serial.print(i + 1);
    Serial.print(": ");
    Serial.println(payload);

    int code =
      http.POST(payload);

    Serial.print(
      "Reconcile response: HTTP "
    );

    Serial.println(code);

    if (code < 0) {

      Serial.print(
        "Reconcile error: "
      );

      Serial.println(
        http.errorToString(code)
      );

    } else if (code >= 200 && code < 300) {

      Serial.println(
        "Device reconciled successfully."
      );

    } else {

      Serial.print(
        "Reconcile rejected: "
      );

      Serial.println(
        http.getString()
      );
    }

    http.end();

    delay(100);
    yield();
  }
}


// ================================================================
// SAVE PERSISTENT STATE
// ================================================================

bool savePersistentState() {

  if (!LittleFS.begin()) {

    Serial.println(
      "ERROR: LittleFS unavailable."
    );

    return false;
  }

  DynamicJsonDocument doc(512);

  JsonArray states =
    doc.createNestedArray("states");

  for (int i = 0; i < DEVICE_COUNT; i++) {

    states.add(
      bulbState[i]
    );
  }

  File file =
    LittleFS.open(
      STATE_FILE,
      "w"
    );

  if (!file) {

    Serial.println(
      "ERROR: Could not open state file for writing."
    );

    return false;
  }

  size_t bytes =
    serializeJson(
      doc,
      file
    );

  file.close();

  if (bytes == 0) {

    Serial.println(
      "ERROR: Failed to write state file."
    );

    return false;
  }

  Serial.println(
    "Relay state saved to LittleFS."
  );

  return true;
}


// ================================================================
// LOAD PERSISTENT STATE
// ================================================================

bool loadPersistentState() {

  if (!LittleFS.begin()) {

    Serial.println(
      "ERROR: LittleFS unavailable."
    );

    return false;
  }

  if (!LittleFS.exists(STATE_FILE)) {

    Serial.println(
      "State file does not exist yet."
    );

    return false;
  }

  File file =
    LittleFS.open(
      STATE_FILE,
      "r"
    );

  if (!file) {

    Serial.println(
      "ERROR: Could not open state file."
    );

    return false;
  }

  DynamicJsonDocument doc(512);

  DeserializationError error =
    deserializeJson(
      doc,
      file
    );

  file.close();

  if (error) {

    Serial.print(
      "ERROR: State JSON parse failed: "
    );

    Serial.println(
      error.c_str()
    );

    return false;
  }

  if (!doc.containsKey("states")) {

    Serial.println(
      "ERROR: State file has no states array."
    );

    return false;
  }

  JsonArray states =
    doc["states"].as<JsonArray>();

  if (states.size() != DEVICE_COUNT) {

    Serial.println(
      "ERROR: Invalid number of saved states."
    );

    return false;
  }

  for (int i = 0; i < DEVICE_COUNT; i++) {

    bulbState[i] =
      states[i].as<bool>();
  }

  return true;
}


// ================================================================
// WIFI CONNECTION
// ================================================================

void connectWiFi() {

  Serial.println(
    "Connecting to WiFi..."
  );

  WiFi.mode(WIFI_STA);

  WiFi.setAutoReconnect(true);
  WiFi.persistent(false);

  WiFi.begin(
    WIFI_SSID,
    WIFI_PASSWORD
  );

  unsigned long start =
    millis();

  while (
    WiFi.status() != WL_CONNECTED &&
    millis() - start < 30000
  ) {

    delay(500);

    Serial.print(".");
    yield();
  }

  Serial.println();

  if (WiFi.status() == WL_CONNECTED) {

    Serial.println(
      "WiFi connected."
    );

    printWiFiStatus();

  } else {

    Serial.println(
      "ERROR: WiFi connection timed out."
    );
  }
}


// ================================================================
// WIFI RECONNECTION
// ================================================================

void reconnectWiFi() {

  if (
    millis() - lastWiFiRetry <
    WIFI_RETRY_INTERVAL
  ) {

    return;
  }

  lastWiFiRetry =
    millis();

  Serial.println();
  Serial.println(
    "WiFi disconnected. Reconnecting..."
  );

  WiFi.disconnect();

  delay(100);

  WiFi.begin(
    WIFI_SSID,
    WIFI_PASSWORD
  );
}


// ================================================================
// WIFI STATUS
// ================================================================

void printWiFiStatus() {

  Serial.print(
    "SSID: "
  );

  Serial.println(
    WiFi.SSID()
  );

  Serial.print(
    "IP: "
  );

  Serial.println(
    WiFi.localIP()
  );

  Serial.print(
    "Signal: "
  );

  Serial.print(
    WiFi.RSSI()
  );

  Serial.println(
    " dBm"
  );

  Serial.print(
    "MAC: "
  );

  Serial.println(
    WiFi.macAddress()
  );
}


// ================================================================
// DEVICE STATUS
// ================================================================

void printDeviceStates() {

  Serial.println();
  Serial.println(
    "Current relay states:"
  );

  for (int i = 0; i < DEVICE_COUNT; i++) {

    Serial.print(
      "Device "
    );

    Serial.print(
      i + 1
    );

    Serial.print(
      " ["
    );

    Serial.print(
      DEVICE_IDS[i]
    );

    Serial.print(
      "] = "
    );

    Serial.println(
      bulbState[i]
        ? "ON"
        : "OFF"
    );
  }
}


// ================================================================
// URL ENCODING
// ================================================================

String urlEncode(
  const String& value
) {

  String encoded;

  const char* hex =
    "0123456789ABCDEF";

  for (size_t i = 0; i < value.length(); i++) {

    unsigned char c =
      static_cast<unsigned char>(
        value[i]
      );

    /*
     * RFC3986 unreserved characters:
     *
     * A-Z
     * a-z
     * 0-9
     * -
     * .
     * _
     * ~
     */

    if (
      (c >= 'A' && c <= 'Z') ||
      (c >= 'a' && c <= 'z') ||
      (c >= '0' && c <= '9') ||
      c == '-' ||
      c == '.' ||
      c == '_' ||
      c == '~'
    ) {

      encoded +=
        static_cast<char>(c);

    } else {

      encoded += '%';

      encoded +=
        hex[(c >> 4) & 0x0F];

      encoded +=
        hex[c & 0x0F];
    }
  }

  return encoded;
}
This is the real production firmware referenced by the dashboard documentation. Configure WIFI_SSID, WIFI_PASSWORD, API_KEY, and API_SECRET with your own credentials, and set DEVICE_IDS / RELAY_PINS according to your wiring. LED_BUILTIN blinks once each time a relay command is executed. Heartbeat sends true relay state every 4 seconds with automatic retry on failure; on boot the device reconciles its persistent LittleFS state with the server so manual wall switches and power losses are always reflected in the dashboard.

Error responses

CodeError codeDescription
401UNAUTHORIZEDInvalid or missing API keys
403ACCESS_DENIEDUser does not own or share the device
404DEVICE_NOT_FOUNDInvalid device ID
429RATE_LIMITToo many requests (max 60/min)
409DEVICE_LIMIT_EXCEEDEDUser already owns 5 devices

Rate limits

Heartbeat endpoints: 20 requests per 10 seconds per device. Control endpoints: 30 requests per minute per user.