HTTP REST API Guide

Complete reference for writing data from devices, reading history, and querying your Virtuino Cloud account.

Base URL
All endpoints are relative to: https://api.virtuino.com/api/data
Devices & Fields must exist first. The API does not create devices or fields automatically. Before sending data, create the device and all its fields in Console → Devices. The names you use in API calls must exactly match the names defined there.
MethodEndpointDescription
POST/writeWrite one or more field values
GET/device-history/{deviceName}History for all fields of a device
GET/device/{deviceName}/field/{field}History for a specific field of a device
GET/field/{field}History for a field without a device
GET/devices-history/History across all devices
GET/historyFlat history (all devices & fields)
GET/device-info  /  /device-info/{name}Device & field inventory

1Authentication

Every request must include your API Key. You can pass it in any of the three ways below — choose whichever fits your client best.

MethodLocationExample
x-api-keyHTTP Headerx-api-key: YOUR_API_KEY
api_keyQuery string?api_key=YOUR_API_KEY
api_keyJSON body{ "api_key": "YOUR_API_KEY", … }

Your API Key is found in Console → API & Connections → HTTP API Key.

Keep your API Key secret. Do not embed it in public source code or client-side JavaScript. Use a separate, read-only key for dashboards and a write-capable key only on trusted devices.

2Write Data   600 req / min per API key

Sends one or more field values to the platform. If publish: true is set, the value is also forwarded to the MQTT broker automatically (requires Essential+ plan).

POST Write a single field value for a device
URL
https://api.virtuino.com/api/data/write
Headers
x-api-key: YOUR_API_KEY
Content-Type: application/json
Body (JSON)
{
  "device_name": "esp32",
  "field":       "temperature",
  "value":       23.75
}
Response
{
  "success": true,
  "count":   1,
  "total":   1,
  "device":  "esp32"
}
POST Write a single field value — no device (standalone field)
URL
https://api.virtuino.com/api/data/write
Headers
x-api-key: YOUR_API_KEY
Content-Type: application/json
Body (JSON)
{
  "field": "global_status",
  "value": 1
}
// device_name omitted → stored under "unspecified"
Response
{
  "success": true,
  "count":   1,
  "total":   1,
  "device":  "unspecified"
}
POST Write multiple fields in one request (data array)
URL
https://api.virtuino.com/api/data/write
Headers
x-api-key: YOUR_API_KEY
Content-Type: application/json
Body (JSON)
{
  "device_name": "greenhouse_01",
  "data": [
    { "field": "temperature", "value": 24.5 },
    { "field": "humidity",    "value": 65   },
    { "field": "soil",         "value": 42   }
  ]
}
Response
{
  "success": true,
  "count":   3,
  "total":   3,
  "device":  "greenhouse_01"
}
POST Write with custom timestamp
URL
https://api.virtuino.com/api/data/write
Headers
x-api-key: YOUR_API_KEY
Content-Type: application/json
Body (JSON)
{
  "device_name": "esp32",
  "time":        "2026-06-12T10:00:00Z",  // ISO 8601 UTC
  "data": [
    { "field": "temperature", "value": 22.1 },
    { "field": "humidity",    "value": 58.4 }
  ]
}
Response
{
  "success": true,
  "count":   2,
  "total":   2,
  "device":  "esp32"
}
POST Write with MQTT publish (HTTP → MQTT Gateway)
URL
https://api.virtuino.com/api/data/write
Headers
x-api-key: YOUR_API_KEY
Content-Type: application/json
Body (JSON)
{
  "device_name": "esp32",
  "data": [
    { "field": "relay1", "value": 1, "publish": true },
    { "field": "relay2", "value": 0, "publish": true }
  ]
}
// publish:true → platform publishes to {sub_key}/out/device/esp32/relay1 etc.
// Requires Essential+ plan (api_gateway feature)
Response
{
  "success": true,
  "count":   2,
  "total":   2,
  "device":  "esp32"
}
POST Response when a field does not exist in the Console
Body sent
{
  "device_name": "esp32",
  "data": [
    { "field": "temperature", "value": 23.5 },
    { "field": "nonexistent",  "value": 99   }
  ]
}
Response
{
  "success":        true,
  "count":          1,
  "total":          2,
  "device":         "esp32",
  "skipped_fields": ["nonexistent"],
  "note":           "Some fields do not exist. Create them in the console first."
}
HTTP → MQTT Gateway: Adding "publish": true to any field entry causes the platform to publish that value to the corresponding MQTT /out/ topic automatically — no MQTT client needed on the device. See the MQTT documentation for topic format details.

3Device History

Returns historical records for all fields of a specific device, grouped by timestamp. Use unspecified as the device name to query standalone fields.

GET Get last 50 records for a device
URL
https://api.virtuino.com/api/data/device-history/esp32
Query Parameters
api_key = YOUR_API_KEY
limit   = 50              // max 5000, default 100
Full URL Example
https://api.virtuino.com/api/data/device-history/esp32?api_key=YOUR_API_KEY&limit=50
Response
{
  "success": true,
  "device":  "esp32",
  "count":   50,
  "history": [
    {
      "time":   "2026-06-12T10:05:00Z",
      "source": "HTTP",
      "fields": [
        { "field": "temperature", "value": "23.75" },
        { "field": "humidity",    "value": "61.2"  }
      ]
    },
    // … more entries
  ]
}
GET Get the latest snapshot (most recent record only)
URL
https://api.virtuino.com/api/data/device-history/esp32
Query Parameters
api_key = YOUR_API_KEY
latest  = true
Full URL Example
https://api.virtuino.com/api/data/device-history/esp32?api_key=YOUR_API_KEY&latest=true
Response
{
  "success": true,
  "device":  "esp32",
  "time":    "2026-06-12T10:05:00Z",
  "source":  "HTTP",
  "fields": [
    { "field": "temperature", "value": "23.75" },
    { "field": "humidity",    "value": "61.2"  }
  ]
}
GET Get records within a time range
URL
https://api.virtuino.com/api/data/device-history/esp32
Query Parameters
api_key = YOUR_API_KEY
from    = 2026-06-12T00:00:00Z   // ISO 8601 UTC
to      = 2026-06-12T23:59:59Z
limit   = 100
Full URL Example
https://api.virtuino.com/api/data/device-history/esp32?api_key=YOUR_API_KEY&from=2026-06-12T00:00:00Z&to=2026-06-12T23:59:59Z&limit=100
Response
{
  "success": true,
  "device":  "esp32",
  "count":   87,
  "history": [ /* … */ ]
}

4Field History

Returns records for a single field. Use the device-scoped URL when the field belongs to a device; use the short form for standalone (no-device) fields.

GET Get history for a field that belongs to a device
URL
https://api.virtuino.com/api/data/device/esp32/field/temperature
Query Parameters
api_key = YOUR_API_KEY
limit   = 100
Full URL Example
https://api.virtuino.com/api/data/device/esp32/field/temperature?api_key=YOUR_API_KEY&limit=100
Response
{
  "success": true,
  "field":   "temperature",
  "device":  "esp32",
  "count":   100,
  "data": [
    { "time": "2026-06-12T10:05:00Z", "value": "23.75", "source": "HTTP" },
    // …
  ]
}
GET Get latest value for a field of a device
URL
https://api.virtuino.com/api/data/device/esp32/field/temperature
Query Parameters
api_key = YOUR_API_KEY
latest  = true
Full URL Example
https://api.virtuino.com/api/data/device/esp32/field/temperature?api_key=YOUR_API_KEY&latest=true
Response
{
  "success":      true,
  "field":        "temperature",
  "device":       "esp32",
  "latest_entry": {
    "time":   "2026-06-12T10:05:00Z",
    "value":  "23.75",
    "source": "HTTP"
  }
}
GET Get records within a time range for a specific field
URL
https://api.virtuino.com/api/data/device/esp32/field/temperature
Query Parameters
api_key = YOUR_API_KEY
from    = 2026-06-12T00:00:00Z
to      = 2026-06-12T23:59:59Z
limit   = 15
Full URL Example
https://api.virtuino.com/api/data/device/esp32/field/temperature?api_key=YOUR_API_KEY&from=2026-06-12T00:00:00Z&to=2026-06-12T23:59:59Z&limit=15
Response
{
  "success": true,
  "field":   "temperature",
  "device":  "esp32",
  "count":   15,
  "data":    [ /* … */ ]
}
GET Get history for a standalone field (no device)
URL
https://api.virtuino.com/api/data/field/global_status
Query Parameters
api_key = YOUR_API_KEY
latest  = true
Full URL Example
https://api.virtuino.com/api/data/field/global_status?api_key=YOUR_API_KEY&latest=true
Response
{
  "success":      true,
  "field":        "global_status",
  "device":       "Standalone Field",
  "latest_entry": {
    "time":   "2026-06-12T08:00:00Z",
    "value":  "1",
    "source": "HTTP"
  }
}

5All Devices History

Returns records across all devices in your account, grouped by device name. Useful for building full-account dashboards.

GET Get the latest snapshot for every device
URL
https://api.virtuino.com/api/data/devices-history/
Query Parameters
api_key = YOUR_API_KEY
latest  = true
Full URL Example
https://api.virtuino.com/api/data/devices-history/?api_key=YOUR_API_KEY&latest=true
Response
{
  "success": true,
  "data": {
    "esp32": {
      "time":   "2026-06-12T10:05:00Z",
      "source": "HTTP",
      "fields": [
        { "field": "temperature", "value": "23.75" },
        { "field": "humidity",    "value": "61.2"  }
      ]
    },
    "greenhouse_01": {
      "time":   "2026-06-12T09:55:00Z",
      "source": "HTTP",
      "fields": [ /* … */ ]
    }
  }
}
GET Get last 200 records across all devices with a time range
URL
https://api.virtuino.com/api/data/devices-history/
Query Parameters
api_key = YOUR_API_KEY
from    = 2026-06-12T00:00:00Z
to      = 2026-06-12T23:59:59Z
limit   = 200              // max 5000, default 100
Full URL Example
https://api.virtuino.com/api/data/devices-history/?api_key=YOUR_API_KEY&from=2026-06-12T00:00:00Z&to=2026-06-12T23:59:59Z&limit=200
Response
{
  "success": true,
  "data": { /* grouped by device name */ }
}

6Flat History

Returns a flat list of all records across all devices and fields, ordered by time descending. Each row includes device name, field name, value, timestamp, and source.

GET Get the last 50 records across everything
URL
https://api.virtuino.com/api/data/history
Query Parameters
api_key = YOUR_API_KEY
limit   = 50              // default 50, max 5000
Full URL Example
https://api.virtuino.com/api/data/history?api_key=YOUR_API_KEY&limit=50
Response
{
  "success": true,
  "count":   50,
  "data": [
    {
      "time":    "2026-06-12T10:05:00Z",
      "device":  "esp32",
      "field":   "temperature",
      "value":   "23.75",
      "source":  "HTTP"
    },
    // …
  ]
}
GET Get records within a time range
URL
https://api.virtuino.com/api/data/history
Query Parameters
api_key = YOUR_API_KEY
from    = 2026-06-12T00:00:00Z
to      = 2026-06-12T23:59:59Z
limit   = 100
Full URL Example
https://api.virtuino.com/api/data/history?api_key=YOUR_API_KEY&from=2026-06-12T00:00:00Z&to=2026-06-12T23:59:59Z&limit=100
Response
{
  "success": true,
  "count":   100,
  "data":    [ /* … */ ]
}

7Device Info & Inventory

Returns metadata for all devices (or a specific one), including the list of fields associated with each. Use this to discover what devices and fields exist in your account.

GET Get all devices and their fields
URL
https://api.virtuino.com/api/data/device-info
Query Parameters
api_key = YOUR_API_KEY
Full URL Example
https://api.virtuino.com/api/data/device-info?api_key=YOUR_API_KEY
Response
{
  "success":       true,
  "devices_count": 2,
  "inventory": [
    {
      "device_name":      "esp32",
      "available_fields": ["temperature", "humidity", "relay1"]
    },
    {
      "device_name":      "greenhouse_01",
      "available_fields": ["soil", "light"]
    }
  ]
}
GET Get info for a single device
URL
https://api.virtuino.com/api/data/device-info/esp32
Query Parameters
api_key = YOUR_API_KEY
Full URL Example
https://api.virtuino.com/api/data/device-info/esp32?api_key=YOUR_API_KEY
Response
{
  "success":          true,
  "device_name":      "esp32",
  "available_fields": ["temperature", "humidity", "relay1"]
}

8Code Examples

Full working examples. Each example writes two fields and reads back the latest snapshot.

ESP32
ESP8266
Arduino Uno WiFi
Python
JavaScript
cURL
// Required libraries — install via Arduino IDE → Library Manager:
//   ArduinoJson  (by Benoit Blanchon)
// WiFi and HTTPClient are built-in for ESP32 — no installation needed.
#include <WiFi.h>
#include <HTTPClient.h>
#include <ArduinoJson.h>

// ── Configuration — replace with your own values ─────────────────────
const char* ssid       = "YOUR_WIFI_SSID";
const char* password   = "YOUR_WIFI_PASSWORD";
const char* apiKey     = "YOUR_API_KEY";       // Console → Keys & Sub Users
const char* writeUrl   = "https://api.virtuino.com/api/data/write";
const char* historyUrl = "https://api.virtuino.com/api/data/device-history/esp32?latest=true";
// Change "esp32" in historyUrl to match your device name in the Console.

// ── Send two sensor readings to the platform ──────────────────────────
void writeData(float temp, float hum) {
    if (WiFi.status() != WL_CONNECTED) return;
    HTTPClient http;
    http.begin(writeUrl);
    http.addHeader("Content-Type", "application/json");

    // Build JSON body — "device_name" must match the device created in the Console
    StaticJsonDocument<256> doc;
    doc["api_key"]     = apiKey;
    doc["device_name"] = "esp32";
    JsonArray data = doc.createNestedArray("data");
    JsonObject f1 = data.createNestedObject();
    f1["field"] = "temperature"; f1["value"] = temp; f1["publish"] = true;  // publish:true also sends to MQTT
    JsonObject f2 = data.createNestedObject();
    f2["field"] = "humidity";    f2["value"] = hum;  f2["publish"] = true;

    String body;
    serializeJson(doc, body);
    int code = http.POST(body);
    Serial.println("Write → HTTP " + String(code));  // 200 = success
    http.end();
}

// ── Read the latest values stored for this device ─────────────────────
void readLatest() {
    if (WiFi.status() != WL_CONNECTED) return;
    HTTPClient http;
    http.begin(historyUrl);
    http.addHeader("x-api-key", apiKey);  // pass API key as header for GET requests
    int code = http.GET();
    String payload = http.getString();
    http.end();

    Serial.println("Read → HTTP " + String(code));
    if (code != 200) {
        Serial.println("Error: " + payload);  // print server error message
        return;
    }

    // Parse the JSON response
    StaticJsonDocument<512> doc;
    DeserializationError err = deserializeJson(doc, payload);
    if (err) {
        Serial.println("JSON parse error: " + String(err.c_str()));
        return;
    }

    // Iterate the "fields" array and print each name/value pair
    // You can also read specific fields: doc["fields"][0]["value"].as<float>()
    JsonArray fields = doc["fields"];
    for (JsonObject f : fields) {
        const char* name  = f["field"];
        const char* value = f["value"];
        Serial.printf("  %s = %s\n", name, value);
    }
}

void setup() {
    Serial.begin(115200);
    WiFi.begin(ssid, password);
    while (WiFi.status() != WL_CONNECTED) { delay(500); Serial.print("."); }
    Serial.println("\nWi-Fi connected");
}

void loop() {
    static unsigned long last = 0;
    if (millis() - last > 15000) {  // send every 15 seconds
        last = millis();
        writeData(random(200, 300) / 10.0f, random(400, 700) / 10.0f);
        readLatest();
    }
}
// Required libraries — install via Arduino IDE → Library Manager:
//   ArduinoJson  (by Benoit Blanchon)
// Board support: "esp8266 by ESP8266 Community" — Boards Manager → update to version 3.x
// setInsecure() requires ESP8266 core ≥ 2.5.0. If you get a compile error, update the board package.
// ESP8266WiFi, ESP8266HTTPClient, and WiFiClientSecure are part of the ESP8266 core — no separate install needed.
#include <ESP8266WiFi.h>
#include <ESP8266HTTPClient.h>
#include <WiFiClientSecure.h>  // required for HTTPS on ESP8266 — ESP32 handles it natively
#include <ArduinoJson.h>

// ── Configuration — replace with your own values ─────────────────────
const char* ssid       = "YOUR_WIFI_SSID";
const char* password   = "YOUR_WIFI_PASSWORD";
const char* apiKey     = "YOUR_API_KEY";       // Console → API & Connections
const char* writeUrl   = "https://api.virtuino.com/api/data/write";
const char* historyUrl = "https://api.virtuino.com/api/data/device-history/esp8266?latest=true";
// Change "esp8266" in historyUrl to match your device name in the Console.

// ESP8266 needs a WiFiClientSecure object passed to http.begin() for HTTPS
WiFiClientSecure client;

// ── Send two sensor readings to the platform ──────────────────────────
void writeData(float temp, float hum) {
    if (WiFi.status() != WL_CONNECTED) return;
    client.setInsecure();  // skip TLS certificate verification — simplest setup for IoT
    HTTPClient http;
    http.begin(client, writeUrl);  // ESP8266 requires the client object; ESP32 does not
    http.addHeader("Content-Type", "application/json");
    // Build JSON body — "device_name" must match the device created in the Console
    StaticJsonDocument<256> doc;
    doc["api_key"]     = apiKey;
    doc["device_name"] = "esp8266";
    JsonArray data = doc.createNestedArray("data");
    JsonObject f1 = data.createNestedObject();
    f1["field"] = "temperature"; f1["value"] = temp; f1["publish"] = true;  // publish:true also sends to MQTT
    JsonObject f2 = data.createNestedObject();
    f2["field"] = "humidity"; f2["value"] = hum; f2["publish"] = true;
    String body;
    serializeJson(doc, body);
    int code = http.POST(body);
    Serial.println("Write → HTTP " + String(code));  // 200 = success
    http.end();
}

// ── Read the latest values stored for this device ─────────────────────
void readLatest() {
    if (WiFi.status() != WL_CONNECTED) return;
    client.setInsecure();
    HTTPClient http;
    http.begin(client, historyUrl);
    http.addHeader("x-api-key", apiKey);  // pass API key as header for GET requests
    int code = http.GET();
    String payload = http.getString();
    http.end();
    Serial.println("Read → HTTP " + String(code));
    if (code != 200) {
        Serial.println("Error: " + payload);  // print server error message
        return;
    }
    // Parse the JSON response
    StaticJsonDocument<512> doc;
    DeserializationError err = deserializeJson(doc, payload);
    if (err) { Serial.println("JSON parse error: " + String(err.c_str())); return; }
    // Iterate the "fields" array and print each name/value pair
    JsonArray fields = doc["fields"];
    for (JsonObject f : fields) {
        const char* name  = f["field"];
        const char* value = f["value"];
        Serial.printf("  %s = %s\n", name, value);
    }
}

void setup() {
    Serial.begin(115200);
    WiFi.begin(ssid, password);
    while (WiFi.status() != WL_CONNECTED) { delay(500); Serial.print("."); }
    Serial.println("\nWi-Fi connected");
}

void loop() {
    static unsigned long last = 0;
    if (millis() - last > 15000) {  // send every 15 seconds
        last = millis();
        writeData(random(200, 300) / 10.0f, random(400, 700) / 10.0f);
        readLatest();
    }
}
// Required libraries — install via Arduino IDE → Library Manager:
//   WiFiNINA  (by Arduino)
//   ArduinoHttpClient  (by Arduino)
//   ArduinoJson  (by Benoit Blanchon)
// Compatible boards: Arduino Uno WiFi Rev2, Arduino MKR WiFi 1010
#include <WiFiNINA.h>
#include <ArduinoHttpClient.h>
#include <ArduinoJson.h>

// ── Configuration — replace with your own values ─────────────────────
const char* ssid     = "YOUR_WIFI_SSID";
const char* password = "YOUR_WIFI_PASSWORD";
const char* apiKey   = "YOUR_API_KEY";        // Console → API & Connections
const char* server   = "api.virtuino.com";    // hostname only — port/path set below

// WiFiSSLClient handles TLS; HttpClient wraps it with the hostname and port
WiFiSSLClient   wifiClient;
HttpClient      http(wifiClient, server, 443);

// ── Send two sensor readings to the platform ──────────────────────────
void writeData(float temp, float hum) {
    // Build JSON body — "device_name" must match the device created in the Console
    StaticJsonDocument<256> doc;
    doc["api_key"]     = apiKey;
    doc["device_name"] = "arduino_uno";
    JsonArray data = doc.createNestedArray("data");
    JsonObject f1 = data.createNestedObject();
    f1["field"] = "temperature"; f1["value"] = temp; f1["publish"] = true;  // publish:true also sends to MQTT
    JsonObject f2 = data.createNestedObject();
    f2["field"] = "humidity"; f2["value"] = hum; f2["publish"] = true;
    String body;
    serializeJson(doc, body);
    // ArduinoHttpClient requires an explicit beginRequest/endRequest sequence
    http.beginRequest();
    http.post("/api/data/write");
    http.sendHeader("Content-Type", "application/json");
    http.sendHeader("Content-Length", body.length());
    http.endRequest();
    http.print(body);
    int code = http.responseStatusCode();
    Serial.println("Write → HTTP " + String(code));  // 200 = success
}

// ── Read the latest values stored for this device ─────────────────────
void readLatest() {
    // Pass API key as a query parameter — ArduinoHttpClient does not support custom GET headers
    String path = String("/api/data/device-history/arduino_uno?api_key=") + apiKey + "&latest=true";
    http.get(path);
    int code = http.responseStatusCode();
    String payload = http.responseBody();
    Serial.println("Read → HTTP " + String(code));
    if (code != 200) { Serial.println("Error: " + payload); return; }
    // Parse JSON and print each field/value pair
    StaticJsonDocument<512> doc;
    if (deserializeJson(doc, payload)) { Serial.println("JSON parse error"); return; }
    JsonArray fields = doc["fields"];
    for (JsonObject f : fields)
        Serial.printf("  %s = %s\n", (const char*)f["field"], (const char*)f["value"]);
}

void setup() {
    Serial.begin(9600);
    while (!Serial);  // wait for Serial Monitor to open (needed on Uno WiFi Rev2)
    while (WiFi.begin(ssid, password) != WL_CONNECTED) { delay(1000); }
    Serial.println("Wi-Fi connected");
}

void loop() {
    static unsigned long last = 0;
    if (millis() - last > 15000) {  // send every 15 seconds
        last = millis();
        writeData(random(200, 300) / 10.0f, random(400, 700) / 10.0f);
        readLatest();
    }
}
# pip install requests
import requests

# ── Configuration — replace with your own values ──────────────────────
API_KEY  = "YOUR_API_KEY"   # Console → API & Connections
BASE_URL = "https://api.virtuino.com/api/data"
DEVICE   = "esp32"          # must match a device name created in the Console

# ── Write two fields ──────────────────────────────────────────────────
# publish:True also pushes the value to the MQTT broker (Essential+ plan)
r = requests.post(f"{BASE_URL}/write", json={
    "api_key":     API_KEY,
    "device_name": DEVICE,
    "data": [
        { "field": "temperature", "value": 23.75, "publish": True },
        { "field": "humidity",    "value": 61.2,  "publish": True }
    ]
})
print("Write:", r.json())  # {"success": true, "message": "Stored 2 fields ..."}

# ── Read latest snapshot for all fields of a device ───────────────────
# Returns {"success": true, "device": ..., "fields": [{"field": ..., "value": ...}, ...]}
r = requests.get(f"{BASE_URL}/device-history/{DEVICE}",
                params={ "api_key": API_KEY, "latest": "true" })
print("Latest:", r.json())

# ── Read last 15 records for one field in a time range ────────────────
# Timestamps must be in ISO 8601 UTC format: YYYY-MM-DDTHH:MM:SSZ
r = requests.get(f"{BASE_URL}/device/{DEVICE}/field/temperature", params={
    "api_key": API_KEY,
    "from":    "2026-06-12T00:00:00Z",
    "to":      "2026-06-12T23:59:59Z",
    "limit":   15
})
print("History:", r.json())
// ── Configuration — replace with your own values ─────────────────────
const API_KEY  = "YOUR_API_KEY";   // Console → API & Connections
const BASE_URL = "https://api.virtuino.com/api/data";

// ── Write two fields ──────────────────────────────────────────────────
// api_key goes in the JSON body for POST requests
async function writeData() {
    const res = await fetch(`${BASE_URL}/write`, {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({
            api_key: API_KEY,
            device_name: "esp32",  // must match a device created in the Console
            data: [
                { field: "temperature", value: 23.75, publish: true },  // publish:true also sends to MQTT
                { field: "humidity",    value: 61.2,  publish: true }
            ]
        })
    });
    const data = await res.json();
    console.log("Write:", data);  // {success: true, message: "Stored 2 fields ..."}
}

// ── Read latest snapshot for all fields of a device ───────────────────
// For GET requests, pass the API key as a query parameter
async function readLatest() {
    const res = await fetch(`${BASE_URL}/device-history/esp32?api_key=${API_KEY}&latest=true`);
    const data = await res.json();
    console.log("Latest:", data);
    // Access individual fields: data.fields[0].value, data.fields[1].value, etc.
}

writeData();
readLatest();
# ── Write two fields ──────────────────────────────────────────────────
# API key goes in the x-api-key header for POST; "device_name" must match the Console
# publish:true also pushes the value to the MQTT broker (Essential+ plan)
curl -X POST https://api.virtuino.com/api/data/write \
     -H "x-api-key: YOUR_API_KEY" \
     -H "Content-Type: application/json" \
     -d '{
       "device_name": "esp32",
       "data": [
         {"field":"temperature","value":23.75,"publish":true},
         {"field":"humidity","value":61.2,"publish":true}
       ]
     }'

# ── Read latest snapshot for all fields of a device ───────────────────
# For GET requests, pass the API key as a query parameter
curl "https://api.virtuino.com/api/data/device-history/esp32?api_key=YOUR_API_KEY&latest=true"

# ── Read last 15 temperature records in a time range ─────────────────
# Timestamps in ISO 8601 UTC: YYYY-MM-DDTHH:MM:SSZ
curl "https://api.virtuino.com/api/data/device/esp32/field/temperature?api_key=YOUR_API_KEY&from=2026-06-12T00:00:00Z&to=2026-06-12T23:59:59Z&limit=15"

# ── Get full account inventory ────────────────────────────────────────
# Returns all devices, their online status, last-seen time, and available fields
curl "https://api.virtuino.com/api/data/device-info?api_key=YOUR_API_KEY"

9Error Codes

All error responses follow the format { "success": false, "error": "…" }.

HTTP StatusMeaningHow to fix
400 Bad Request Invalid JSON, missing required field, or invalid characters in device_name. Check the request body and URL for typos.
401 Unauthorized No API key provided. Add x-api-key header, ?api_key=, or "api_key" in body.
403 Forbidden API key is invalid or does not have write permission. Check your key in Console → API & Connections.
404 Not Found Device or field does not exist. Create the device and field in Console → Devices first.
429 Too Many Requests Rate limit exceeded (600 writes/min per API key). Wait the number of seconds in the Retry-After response header.
500 Server Error Internal error on the platform. Retry after a short delay. Contact support if the issue persists.