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 API Hub

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 API Hub 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 firmware example with auto‑reconnect, state sync, and command queue processing.

/*
 * NodeMCU Smart Bulb Controller (4 Devices) - State Persistence
 * ESP8266 / NodeMCU
 * Full integration with Smart Dashboard API
 *
 * Features:
 *   - 4 independently controlled relay outputs
 *   - HTTPS communication with ODIVORA Smart Dashboard API
 *   - Persistent relay state using LittleFS
 *   - Device heartbeat with retry on failure
 *   - Remote ON / OFF / TOGGLE commands
 *   - State confirmation
 *   - Device reconciliation after boot
 *   - Automatic WiFi reconnection
 *   - HTTP/JSON error handling
 *   - Request timeouts
 */
#include <ESP8266WiFi.h>
#include <ESP8266HTTPClient.h>
#include <WiFiClientSecure.h>
#include <LittleFS.h>
#include <ArduinoJson.h>

// WiFi & Server config
const char* WIFI_SSID     = "YOUR_WIFI_SSID";
const char* WIFI_PASSWORD = "YOUR_WIFI_PASSWORD";
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";

#define DEVICE_COUNT 4
const char* DEVICE_IDS[DEVICE_COUNT] = {
  "SD-A42EACCBB767", "SD-64316F62789F",
  "SD-5D22DC343C9B", "SD-E2C60A2E5EDD"
};
const int RELAY_PINS[DEVICE_COUNT] = { D1, D2, D5, D6 };
const bool RELAY_ACTIVE_LOW = false;

// Timing - heartbeats every 4s, retries every 2s
const unsigned long HEARTBEAT_INTERVAL = 4000;
const unsigned long COMMAND_INTERVAL = 1500;
const unsigned long WIFI_RETRY_INTERVAL = 5000;
const unsigned long HEARTBEAT_RETRY_INTERVAL = 2000;
const unsigned long HTTP_TIMEOUT = 10000;

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

// Runtime state
bool bulbState[DEVICE_COUNT] = { false, false, false, false };
String lastCommand[DEVICE_COUNT];
unsigned long lastHeartbeat = 0, lastCmdCheck = 0;
unsigned long lastWiFiRetry = 0, lastHeartbeatRetry = 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 executeCommand(int idx, const String& cmd);
void sendAllHeartbeats();
void retryFailedHeartbeats();
void checkAllCommands();
void confirmState(int idx);
void reconcileAllDevices();
bool loadPersistentState();
bool savePersistentState();
bool beginHttp(HTTPClient& http, const String& path);
String urlEncode(const String& value);

void setup() {
  Serial.begin(115200);
  delay(200);
  
  Serial.println();
  Serial.println("================================================");
  Serial.println(" ODIVORA NODEMCU SMART BULB CONTROLLER");
  Serial.println("================================================");

  useTLS = String(SERVER_URL).startsWith("https://");
  if (useTLS) {
    secureClient.setInsecure();
    secureClient.setTimeout(HTTP_TIMEOUT / 1000);
  } else {
    plainClient.setTimeout(HTTP_TIMEOUT / 1000);
  }

  // Initialize LittleFS
  if (!LittleFS.begin()) {
    LittleFS.format();
    LittleFS.begin();
  }

  // Load saved relay states
  if (!loadPersistentState()) {
    for (int i = 0; i < DEVICE_COUNT; i++) bulbState[i] = false;
  }

  // Configure relay pins
  for (int i = 0; i < DEVICE_COUNT; i++) {
    pinMode(RELAY_PINS[i], OUTPUT);
    setRelay(i, bulbState[i]);
  }

  connectWiFi();
  if (WiFi.status() == WL_CONNECTED) reconcileAllDevices();
}

void loop() {
  if (WiFi.status() != WL_CONNECTED) {
    reconnectWiFi();
    delay(10);
    return;
  }

  // Send heartbeats every 4 seconds
  if (millis() - lastHeartbeat >= HEARTBEAT_INTERVAL) {
    lastHeartbeat = millis();
    sendAllHeartbeats();
  }

  // Retry failed heartbeats every 2 seconds
  if (millis() - lastHeartbeatRetry >= HEARTBEAT_RETRY_INTERVAL) {
    lastHeartbeatRetry = millis();
    retryFailedHeartbeats();
  }

  // Check commands every 1.5 seconds
  if (millis() - lastCmdCheck >= COMMAND_INTERVAL) {
    lastCmdCheck = millis();
    checkAllCommands();
  }

  yield();
}

bool beginHttp(HTTPClient& http, const String& path) {
  String url = String(SERVER_URL) + path;
  http.setTimeout(HTTP_TIMEOUT);
  if (useTLS) return http.begin(secureClient, url);
  return http.begin(plainClient, url);
}

void sendAllHeartbeats() {
  for (int i = 0; i < DEVICE_COUNT; i++) {
    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();
      heartbeatFailed[i] = true;
      continue;
    }

    http.addHeader("X-API-Key", API_KEY);
    http.addHeader("X-API-Secret", API_SECRET);
    http.addHeader("Accept", "application/json");

    int code = http.GET();
    if (code >= 200 && code < 300) {
      heartbeatFailed[i] = false;
    } else {
      heartbeatFailed[i] = true;
    }
    http.end();
    delay(30);
    yield();
  }
}

void retryFailedHeartbeats() {
  bool anyFailed = false;
  for (int i = 0; i < DEVICE_COUNT; i++) {
    if (heartbeatFailed[i]) { anyFailed = true; break; }
  }
  if (!anyFailed) return;

  for (int i = 0; i < DEVICE_COUNT; i++) {
    if (!heartbeatFailed[i]) continue;
    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(); continue; }

    http.addHeader("X-API-Key", API_KEY);
    http.addHeader("X-API-Secret", API_SECRET);
    http.addHeader("Accept", "application/json");

    int code = http.GET();
    if (code >= 200 && code < 300) heartbeatFailed[i] = false;
    http.end();
    delay(30);
    yield();
  }
}

void checkAllCommands() {
  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]) +
      "/status?wifi_board_id=" + urlEncode(WIFI_BOARD_ID);

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

    http.addHeader("X-API-Key", API_KEY);
    http.addHeader("X-API-Secret", API_SECRET);
    http.addHeader("Accept", "application/json");

    int code = http.GET();
    if (code == 200) {
      DynamicJsonDocument doc(1024);
      if (!deserializeJson(doc, http.getString())) {
        if (doc.containsKey("command") && !doc["command"].isNull()) {
          String cmd = doc["command"].as<String>();
          cmd.trim();
          cmd.toLowerCase();
          if (cmd.length() > 0 && cmd != lastCommand[i]) {
            lastCommand[i] = cmd;
            executeCommand(i, cmd);
          }
        }
      }
    }
    http.end();
    delay(50);
    yield();
  }
}

void executeCommand(int idx, const String& cmd) {
  if (idx < 0 || idx >= DEVICE_COUNT) return;

  bool newState;
  if (cmd == "on") newState = true;
  else if (cmd == "off") newState = false;
  else if (cmd == "toggle") newState = !bulbState[idx];
  else return;

  bulbState[idx] = newState;
  setRelay(idx, newState);
  savePersistentState();
  confirmState(idx);
}

void confirmState(int idx) {
  if (WiFi.status() != WL_CONNECTED) 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("X-API-Key", API_KEY);
  http.addHeader("X-API-Secret", API_SECRET);
  http.POST(payload);
  http.end();
}

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("X-API-Key", API_KEY);
    http.addHeader("X-API-Secret", API_SECRET);
    http.POST(payload);
    http.end();
    delay(100);
    yield();
  }
}

void setRelay(int idx, bool on) {
  if (idx < 0 || idx >= DEVICE_COUNT) return;
  int outputState = RELAY_ACTIVE_LOW ? (on ? LOW : HIGH) : (on ? HIGH : LOW);
  digitalWrite(RELAY_PINS[idx], outputState);
}

bool savePersistentState() {
  if (!LittleFS.begin()) 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) return false;

  size_t bytes = serializeJson(doc, file);
  file.close();
  return bytes > 0;
}

bool loadPersistentState() {
  if (!LittleFS.begin()) return false;
  if (!LittleFS.exists(STATE_FILE)) return false;

  File file = LittleFS.open(STATE_FILE, "r");
  if (!file) return false;

  DynamicJsonDocument doc(512);
  if (deserializeJson(doc, file)) { file.close(); return false; }
  file.close();

  if (!doc.containsKey("states")) return false;
  JsonArray states = doc["states"].as<JsonArray>();
  if (states.size() != DEVICE_COUNT) return false;

  for (int i = 0; i < DEVICE_COUNT; i++) bulbState[i] = states[i].as<bool>();
  return true;
}

void connectWiFi() {
  WiFi.mode(WIFI_STA);
  WiFi.setAutoReconnect(true);
  WiFi.begin(WIFI_SSID, WIFI_PASSWORD);
  unsigned long start = millis();
  while (WiFi.status() != WL_CONNECTED && millis() - start < 30000) {
    delay(500);
    yield();
  }
}

void reconnectWiFi() {
  if (millis() - lastWiFiRetry < WIFI_RETRY_INTERVAL) return;
  lastWiFiRetry = millis();
  WiFi.disconnect();
  delay(100);
  WiFi.begin(WIFI_SSID, WIFI_PASSWORD);
}

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]);
    if (isalnum(c) || c == '-' || c == '.' || c == '_' || c == '~') {
      encoded += static_cast<char>(c);
    } else {
      encoded += '%';
      encoded += hex[(c >> 4) & 0x0F];
      encoded += hex[c & 0x0F];
    }
  }
  return encoded;
}
The firmware uses ArduinoJson and ESP8266HTTPClient with WiFiClientSecure because the production API is HTTPS-only. Configure DEVICE_IDS and RELAY_PINS according to your wiring. Heartbeat sends every 4 seconds with automatic retry on failure. On ESP32, replace WiFiClientSecure/secureClient.setInsecure() with the ESP32 WiFiClientSecure API (same names).

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.