Adafruit IO — Send Sensor Data to a Cloud Dashboard

How to send ESP32 sensor data to Adafruit IO via HTTP and build a live dashboard with gauges and charts

Adafruit IO is a free cloud service that makes it easy to get your sensor data onto a web dashboard — no backend coding, no server management. In this tutorial, you will create feeds for temperature, humidity, and pressure, publish data from a BME280 sensor via HTTP, and build a live dashboard with charts and gauges. Everything here works on the free tier.

🔗What is Adafruit IO?

Adafruit IO is a cloud platform built by Adafruit for IoT projects. It accepts data via MQTT or a simple HTTP REST API, stores it, and lets you visualize it with built-in dashboard widgets.

Key features of the free tier:

FeatureFree Tier Limit
Data rate30 data points per minute (across all feeds)
Data retention30 days
Dashboards5
Feeds10
Triggers (alerts)5

The free tier is generous enough for most hobby projects. A single ESP32 sending temperature, humidity, and pressure every 30 seconds uses only 6 data points per minute — well within the limit.

🔗What You'll Need

ComponentQtyNotesBuy
ESP32 dev board1AliExpress | Amazon.de .co.uk .com
BME280 sensor module1AliExpress | Amazon.de .co.uk .com
Breadboard1AliExpress | Amazon.de .co.uk .com
Jumper wires~4Male-to-maleAliExpress | Amazon.de .co.uk .com

Links marked Amazon/AliExpress are affiliate links. We may earn a small commission at no extra cost to you.

You will also need:

  • A free Adafruit IO account — sign up at io.adafruit.com
  • The Arduino IDE with ESP32 board support installed

🔗Required Libraries

Install the following through the Arduino Library Manager (Sketch > Include Library > Manage Libraries...):

  1. Adafruit BME280 Library by Adafruit
  2. Adafruit Unified Sensor by Adafruit (installed as a dependency)
  3. ArduinoJson by Benoit Blanchon (for the multi-feed example)

The HTTP requests use the built-in WiFi.h and HTTPClient.h libraries — no extra installation needed for those.

🔗Account Setup

  1. Go to io.adafruit.com and create a free account.
  2. Once logged in, click the key icon (or go to Settings > My Key) to find your AIO Key. You will need two values:
    • Username — your Adafruit IO username
    • Active Key — a long string like aio_AbCd1234...
  3. Create a new Feed called temperature (go to Feeds > New Feed).

Warning: Your AIO Key grants full access to your account. Treat it like a password — do not commit it to a public repository or share it in screenshots.

🔗Wiring

Connect the BME280 to your ESP32 using I2C. This is the same wiring used in the BME280 guide.

BME280 PinESP32 PinNotes
VIN / VCC3.3VUse 3.3V (most modules have an onboard regulator)
GNDGND
SDAGPIO 21Default I2C data line on ESP32
SCLGPIO 22Default I2C clock line on ESP32

Pin labels and GPIO numbers vary between ESP32 boards. GPIOs 21 (SDA) and 22 (SCL) are the defaults on most ESP32-WROOM-32 DevKit boards. Check your board's pinout diagram if readings fail.

🔗Code Example: HTTP POST to Adafruit IO

This sketch connects to WiFi, reads the BME280 sensor, and sends the temperature to your Adafruit IO feed every 30 seconds using an HTTP POST request.

View complete sketch
#include <WiFi.h>
#include <HTTPClient.h>
#include <Wire.h>
#include <Adafruit_Sensor.h>
#include <Adafruit_BME280.h>

// ----- WiFi credentials -----
const char* ssid     = "YOUR_WIFI_SSID";
const char* password = "YOUR_WIFI_PASSWORD";

// ----- Adafruit IO credentials -----
const char* aioUsername = "YOUR_AIO_USERNAME";
const char* aioKey      = "YOUR_AIO_KEY";
const char* feedName    = "temperature";

// ----- Sensor -----
#define BME280_ADDRESS 0x76  // Change to 0x77 if needed
Adafruit_BME280 bme;

// ----- Timing -----
unsigned long lastSendTime = 0;
const unsigned long sendInterval = 30000;  // 30 seconds

void connectToWiFi() {
  Serial.printf("Connecting to %s", ssid);
  WiFi.mode(WIFI_STA);
  WiFi.begin(ssid, password);

  int attempts = 0;
  while (WiFi.status() != WL_CONNECTED && attempts < 20) {
    delay(500);
    Serial.print(".");
    attempts++;
  }

  if (WiFi.status() == WL_CONNECTED) {
    Serial.printf("\nConnected! IP: %s\n",
                  WiFi.localIP().toString().c_str());
  } else {
    Serial.println("\nFailed to connect to WiFi.");
  }
}

void sendToAdafruitIO(const char* feed, float value) {
  if (WiFi.status() != WL_CONNECTED) {
    Serial.println("WiFi not connected. Skipping send.");
    return;
  }

  HTTPClient http;

  // Build the URL: https://io.adafruit.com/api/v2/{username}/feeds/{feed}/data
  String url = "https://io.adafruit.com/api/v2/";
  url += aioUsername;
  url += "/feeds/";
  url += feed;
  url += "/data";

  http.begin(url);
  http.addHeader("Content-Type", "application/json");
  http.addHeader("X-AIO-Key", aioKey);

  // Send the value as JSON
  String payload = "{\"value\":" + String(value, 1) + "}";
  int httpCode = http.POST(payload);

  if (httpCode == 200) {
    Serial.printf("Sent to '%s': %.1f\n", feed, value);
  } else {
    Serial.printf("Error sending to '%s': HTTP %d\n", feed, httpCode);
    Serial.println(http.getString());
  }

  http.end();
}

void setup() {
  Serial.begin(115200);
  delay(1000);

  connectToWiFi();

  if (!bme.begin(BME280_ADDRESS)) {
    Serial.println("Error: Could not find BME280 sensor.");
    Serial.println("Check wiring or try address 0x77.");
    while (1) delay(10);
  }

  Serial.println("BME280 sensor found. Starting data upload.");
}

void loop() {
  // Reconnect WiFi if needed
  if (WiFi.status() != WL_CONNECTED) {
    Serial.println("WiFi lost. Reconnecting...");
    connectToWiFi();
    return;
  }

  unsigned long now = millis();
  if (now - lastSendTime >= sendInterval) {
    lastSendTime = now;

    float temperature = bme.readTemperature();
    Serial.printf("Temperature: %.1f °C\n", temperature);

    sendToAdafruitIO(feedName, temperature);
  }
}

🔗How the Code Works

  1. WiFi connection — the connectToWiFi() function connects to your network with a 10-second timeout, following the same pattern from the WiFi basics guide.

  2. BME280 initialization — the sensor is set up in setup() using I2C. If it is not found, the sketch halts with an error message.

  3. HTTP POST — the sendToAdafruitIO() function builds the Adafruit IO REST API URL, adds the X-AIO-Key header for authentication, and sends the sensor value as a JSON payload.

  4. Timing — data is sent every 30 seconds using millis(), which avoids blocking the loop. This is well within the free tier's 30 data points per minute limit.

Tip: The free tier allows 30 data points per minute across all feeds combined. If you are sending temperature, humidity, and pressure every 30 seconds, that is 6 points per minute — plenty of headroom. But if you set the interval to 5 seconds across three feeds, you will hit the limit (36 points/minute) and start getting 429 errors.

🔗Building a Dashboard

Once data is flowing into your feed, you can visualize it:

  1. In Adafruit IO, go to Dashboards > New Dashboard. Give it a name (e.g., "ESP32 Environment").
  2. Open the dashboard and click the gear icon, then Create New Block.
  3. Choose Line Chart and select your temperature feed. This gives you a time-series graph of temperature over time.
  4. Click Create New Block again and choose Gauge. Select the temperature feed, set the min/max range (e.g., $-10$ to $50$), and choose a color scheme.
  5. Arrange the blocks by dragging them around. Click the gear icon and choose Edit Layout to resize them.

The dashboard updates in real time — as your ESP32 sends new data points, the chart and gauge update within seconds. You can share the dashboard link with others, and it works on mobile browsers too.

🔗Sending Multiple Feeds

Most sensor projects need more than one data stream. The BME280 gives us temperature, humidity, and pressure — let's send all three to separate feeds.

First, create three feeds in Adafruit IO:

  • temperature
  • humidity
  • pressure

Then use this modified sketch:

View complete sketch — multiple feeds
#include <WiFi.h>
#include <HTTPClient.h>
#include <Wire.h>
#include <Adafruit_Sensor.h>
#include <Adafruit_BME280.h>

// ----- WiFi credentials -----
const char* ssid     = "YOUR_WIFI_SSID";
const char* password = "YOUR_WIFI_PASSWORD";

// ----- Adafruit IO credentials -----
const char* aioUsername = "YOUR_AIO_USERNAME";
const char* aioKey      = "YOUR_AIO_KEY";

// ----- Feed names -----
const char* feedTemperature = "temperature";
const char* feedHumidity    = "humidity";
const char* feedPressure    = "pressure";

// ----- Sensor -----
#define BME280_ADDRESS 0x76
Adafruit_BME280 bme;

// ----- Timing -----
unsigned long lastSendTime = 0;
const unsigned long sendInterval = 30000;  // 30 seconds

void connectToWiFi() {
  Serial.printf("Connecting to %s", ssid);
  WiFi.mode(WIFI_STA);
  WiFi.begin(ssid, password);

  int attempts = 0;
  while (WiFi.status() != WL_CONNECTED && attempts < 20) {
    delay(500);
    Serial.print(".");
    attempts++;
  }

  if (WiFi.status() == WL_CONNECTED) {
    Serial.printf("\nConnected! IP: %s\n",
                  WiFi.localIP().toString().c_str());
  } else {
    Serial.println("\nFailed to connect to WiFi.");
  }
}

bool sendToAdafruitIO(const char* feed, float value) {
  if (WiFi.status() != WL_CONNECTED) return false;

  HTTPClient http;

  String url = "https://io.adafruit.com/api/v2/";
  url += aioUsername;
  url += "/feeds/";
  url += feed;
  url += "/data";

  http.begin(url);
  http.addHeader("Content-Type", "application/json");
  http.addHeader("X-AIO-Key", aioKey);

  String payload = "{\"value\":" + String(value, 1) + "}";
  int httpCode = http.POST(payload);

  bool success = (httpCode == 200);
  if (!success) {
    Serial.printf("Error sending to '%s': HTTP %d\n", feed, httpCode);
  }

  http.end();
  return success;
}

void setup() {
  Serial.begin(115200);
  delay(1000);

  connectToWiFi();

  if (!bme.begin(BME280_ADDRESS)) {
    Serial.println("Error: Could not find BME280 sensor.");
    Serial.println("Check wiring or try address 0x77.");
    while (1) delay(10);
  }

  Serial.println("BME280 found. Sending to 3 Adafruit IO feeds.");
}

void loop() {
  if (WiFi.status() != WL_CONNECTED) {
    Serial.println("WiFi lost. Reconnecting...");
    connectToWiFi();
    return;
  }

  unsigned long now = millis();
  if (now - lastSendTime >= sendInterval) {
    lastSendTime = now;

    float temperature = bme.readTemperature();
    float humidity    = bme.readHumidity();
    float pressure    = bme.readPressure() / 100.0F;  // Pa to hPa

    Serial.printf("Temp: %.1f °C | Humidity: %.1f %% | Pressure: %.1f hPa\n",
                  temperature, humidity, pressure);

    sendToAdafruitIO(feedTemperature, temperature);
    sendToAdafruitIO(feedHumidity, humidity);
    sendToAdafruitIO(feedPressure, pressure);
  }
}

This sketch calls sendToAdafruitIO() three times per cycle — once for each feed. At a 30-second interval, that is 6 data points per minute, leaving plenty of room within the free tier's limit.

🔗Group Feed Alternative

If you want to send all values in a single HTTP request (using only 1 data point instead of 3), Adafruit IO supports group feeds. Create a group, add your feeds to it, then POST JSON with all values at once:

void sendGroupData(float temp, float hum, float pres) {
  if (WiFi.status() != WL_CONNECTED) return;

  HTTPClient http;

  String url = "https://io.adafruit.com/api/v2/";
  url += aioUsername;
  url += "/groups/environment/data";

  http.begin(url);
  http.addHeader("Content-Type", "application/json");
  http.addHeader("X-AIO-Key", aioKey);

  // Send all feeds in one request
  String payload = "{\"feeds\":[";
  payload += "{\"key\":\"temperature\",\"value\":\"" + String(temp, 1) + "\"},";
  payload += "{\"key\":\"humidity\",\"value\":\"" + String(hum, 1) + "\"},";
  payload += "{\"key\":\"pressure\",\"value\":\"" + String(pres, 1) + "\"}";
  payload += "]}";

  int httpCode = http.POST(payload);
  Serial.printf("Group send: HTTP %d\n", httpCode);

  http.end();
}

This approach is more efficient with your rate limit — one request sends all three values. Create the group in the Adafruit IO web interface under Feeds > New Group, then add your existing feeds to it.

🔗Receiving Commands (Optional)

Adafruit IO feeds are bidirectional — you can also read values back. This lets you control your ESP32 from the dashboard. For example, you could create a Toggle feed and add a toggle button to your dashboard, then poll the feed from your ESP32 to turn an LED on or off.

Here is the basic pattern for polling a feed via HTTP GET:

String getFromAdafruitIO(const char* feed) {
  if (WiFi.status() != WL_CONNECTED) return "";

  HTTPClient http;

  String url = "https://io.adafruit.com/api/v2/";
  url += aioUsername;
  url += "/feeds/";
  url += feed;
  url += "/data/last";

  http.begin(url);
  http.addHeader("X-AIO-Key", aioKey);

  int httpCode = http.GET();
  String result = "";

  if (httpCode == 200) {
    // The response is JSON — extract the "value" field
    String response = http.getString();
    int valueStart = response.indexOf("\"value\":\"") + 9;
    int valueEnd = response.indexOf("\"", valueStart);
    result = response.substring(valueStart, valueEnd);
  }

  http.end();
  return result;
}

You could call this in your loop() every few seconds and check whether the value is "1" (on) or "0" (off). For more responsive control, consider using MQTT instead of HTTP polling — MQTT delivers messages instantly when a feed value changes.

🔗Troubleshooting

ProblemPossible CauseSolution
HTTP 401 (Unauthorized)Wrong AIO KeyDouble-check the key in Settings > My Key — make sure you copied the full string
HTTP 403 (Forbidden)Wrong username or feed nameVerify the username and feed key match exactly (feed keys are lowercase, use hyphens not spaces)
HTTP 429 (Too Many Requests)Exceeded free tier rate limitIncrease the send interval — 30 data points/min across all feeds
Data not appearing on dashboardFeed name mismatchThe feed key (shown in URL) may differ from the display name — use the key
WiFi connection failsWrong credentials or weak signalCheck SSID/password; try moving closer to the router
BME280 not foundWrong I2C address or wiringTry address 0x77; verify SDA/SCL connections
Values appear but chart is flatAll values are the sameBreathe on the sensor to change temperature/humidity and confirm updates

🔗What's Next?