esp32 intermediate 30 min

ESP32: send HTTPS requests with root CA validation

Make the ESP32 talk to real APIs over HTTPS, with proper TLS validation. The right way to send sensor data to a cloud service.

Code available for: ESP32 Arduino
Published Aug 25, 2026

The ESP32 can talk to HTTPS APIs the same way your laptop does, but the default Arduino HTTP client does not validate TLS certificates. That is fine for testing. It is also how you ship data to the wrong server if someone MITMs your network.

This tutorial covers the right way: load the root CA certificate, validate the chain, and send a real HTTPS request. By the end you can talk to anything that has an HTTPS endpoint, with the security guarantees your laptop gets by default.

What you need

  • ESP32 dev board
  • A Wi-Fi network
  • A web service to talk to. For testing, use <httpbin.org> or a public weather API.

The library

The Arduino HTTPClient library works for HTTPS, but you need to pass it the root CA certificate. For the modern ESP32 Arduino core, the WiFiClientSecure class handles TLS.

#include <WiFi.h>
#include <WiFiClientSecure.h>
#include <HTTPClient.h>

Finding the root CA

The “root CA” is the certificate at the top of the chain that signed the server’s certificate. Your laptop trusts hundreds of root CAs because they come with the OS. The ESP32 has no such trust store.

For a specific server, you can extract the root CA. Visit the site in your browser, click the lock icon, view the certificate, and copy the “Issuer” or “Root CA” certificate. Save it as a PEM file.

For <httpbin.org>, the root CA is “ISRG Root X1” (Let’s Encrypt). For most public APIs, the same root CA applies.

The certificate in code

The root CA goes in your sketch as a string constant. The PEM file looks like this:

-----BEGIN CERTIFICATE-----
MIIFazCCA1OgAwIBAgIRAIIQz7DSQONZRGPgu2OCiwAwDQYJKoZIhvcNAQELBQAw
TzELMAkGA1UEBhMCVVMxKTAnBgNVBAoTIEludGVybmV0IFNlY3VyaXR5IFJlc2Vh
cmNoIEdyb3VwMRUwEwYDVQQDEwxJU1JHIFJvb3QgWDEwHhcNMTUwNjA0MTEwNDM4
WhcNMzUwNjA0MTEwNDM4WjBPMQswCQYDVQQGEwJVUzEpMCcGA1UEChMgSW50ZXJu
ZXQgU2VjdXJpdHkgUmVzZWFyY2ggR3JvdXAxFTATBgNVBAMTDElTUkcgUm9vdCBY
MTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANbOLggKv+IxTdGNs8/TGFy0PTP6DHTha2jSi08zmYjLEzG8hxB9Z8RNlvESvCY3jDFcPlLBzCqKVSjnl2yfNZ9j8+lkfDyLvAhWWpdvk9+ipLZ4R3H8B5z8G8nOMN1Wjj4lEd1ZAC3gpF1Z5l4bffTX+Vw4nBvdKj1zlHI8W9JWAaRFWQybd3uFi3dINl8QG5g8uGFsL8fTQG0d4cPgrJptliTVjpaB0BxLx3L2VPVj7V9Y5DlcGzumckAVyU4XbB+IfSOvk2c0i2tOcX8GQOELcG0aXfXMxPnoC33fOzayVjd39L8J+JP9DwQ4oehb4Meo5gXrpmt0TbOladTdU0CAwEAAaOCAW8wggFrMB8GA1UdIwQYMBaAFFrQC4H1LP7CGnWt1b4o8AKi//n3MC4GA1UdEQQnMCSCDnRtcC5ocmVzdGFwaS5nby5wb3AubmV0ggxocmVzdGFwaS5nbzAdBgNVHQ4EFgQUFrQC4H1LP7CGnWt1b4o8AKi//n3MMAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQELBQADggEBABnTDPEF+3iSP0hNrLDhZx2mnqypJ5YIrO4dZ2EqK4n8U7OE8JbV6MSAgwqavTQ2VNjtVpsBY5q+8uC1A1xLqnJF1y7Ct4CYRZCiH2yO9PE3y8jVBPwFF1bk2WpHvwG9N30EbCeqgxqqFJ1Kt9LkP/6aJTn2tcLR2K9h4d//QAlzJBuY7dHBQ11l5THlpimCv7trSQR4iHhfq6MU8U+wVMKGOAyXXJLxNe2BC0MWhqQGA1bjXJ5A5SrHF36lZePzPZke5nOFOpV/0nNw4ypB9oyhatESdttRNp12NiV4VAK3fW1QUu7HekmTIRuZzU2BdMhp9L8VRYuJSaPZ8d+mRy4c8fJ7VBY5uvT8nA0CAQOjgfwwgfkwHQYDVR0OBBYEFI0cxb6VTEM8YYY6FbBMvAPyT+CyMB8GA1UdIwQYMBaAFI0cxb6VTEM8YYY6FbBMvAPyT+CyMA8GA1UdEwEB/wQFMAMBAf8wggE6BgNVHSAEggE6MIIBNjAYBg1UdEQQwMC6CDnRtcC5ocmVzdGFwaS5nbzAJBgVngQwBAgMwgfQGCyqGSIb3DQEJEAIBBIGWCmCGaEExBwQFBgc=
-----END CERTIFICATE-----

Save this as a C string in your sketch.

The code: HTTPS GET

#include <WiFi.h>
#include <WiFiClientSecure.h>
#include <HTTPClient.h>

const char* ssid = "your-wifi-ssid";
const char* password = "your-wifi-password";

// httpbin.org root CA (Let's Encrypt ISRG Root X1)
const char* rootCA = \
"-----BEGIN CERTIFICATE-----\n" \
"MIIFazCCA1OgAwIBAgIRAIIQz7DSQONZRGPgu2OCiwAwDQYJKoZIhvcNAQELBQAw" \
... (full cert as above) ...
"-----END CERTIFICATE-----\n";

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

  WiFi.begin(ssid, password);
  while (WiFi.status() != WL_CONNECTED) delay(500);
  Serial.println("Connected to Wi-Fi");

  WiFiClientSecure *client = new WiFiClientSecure();
  client->setCACert(rootCA);

  HTTPClient http;
  http.begin(*client, "https://httpbin.org/get");
  int httpCode = http.GET();

  if (httpCode > 0) {
    Serial.print("HTTP ");
    Serial.println(httpCode);
    Serial.println(http.getString());
  } else {
    Serial.print("Error: ");
    Serial.println(http.errorToString(httpCode).c_str());
  }

  http.end();
}

void loop() {
}

Upload. Open Serial Monitor. You should see:

Connected to Wi-Fi
HTTP 200
{
  "args": {},
  "headers": {
    "Accept": "*/*",
    ...
  },
  "origin": "192.168.1.42",
  "url": "https://httpbin.org/get"
}

If you see “connection refused” or a TLS error, the root CA is wrong or out of date. Certificates expire every few years; you may need to refresh the PEM.

The insecure option (do not ship this)

For development, you can disable TLS validation entirely:

client->setInsecure();   // <-- skips all certificate validation

The connection is still encrypted, but you have no guarantee that you are talking to the server you think you are. Use this for prototyping only. Ship with setCACert(rootCA) so a man-in-the-middle cannot intercept your data.

Posting JSON to an API

For sending sensor data, you POST a JSON body:

http.begin(*client, "https://api.example.com/sensors/esp32-1");
http.addHeader("Content-Type", "application/json");
http.addHeader("Authorization", "Bearer your-token-here");

String payload = "{\"temperature\":22.5,\"humidity\":45.2}";
int httpCode = http.POST(payload);

if (httpCode == 200 || httpCode == 201) {
  Serial.println("Posted successfully");
} else {
  Serial.print("Failed: ");
  Serial.println(httpCode);
}

Most cloud APIs (AWS IoT, Google Cloud IoT, Adafruit IO, custom REST APIs) accept JSON over HTTPS. The pattern is the same for all of them.

Memory considerations

Each HTTPS connection uses about 30 KB of RAM for the TLS state. The ESP32 has about 320 KB total. If you have many simultaneous connections or large JSON payloads, watch the memory:

Serial.print("Free heap: ");
Serial.println(ESP.getFreeHeap());

If free heap drops below 30 KB, you are about to crash. Strategies:

  • Make HTTPS requests sequentially, not in parallel.
  • Stream large responses to a buffer you control.
  • Use HTTP/1.1 with Connection: close so the connection is torn down after each request.

When the API requires a client certificate

Some APIs (mostly enterprise or B2B) require mutual TLS: the server verifies the client’s certificate too. The ESP32 can do this:

client->setCACert(rootCA);
client->setCertificate(clientCert);
client->setPrivateKey(clientKey);

The client certificate and private key are PEM-encoded. You generate them on the server side (or via OpenSSL on your laptop) and embed them in the firmware. For device fleets, you sign the client cert with your own CA so you can revoke it.

What you learned

  • WiFiClientSecure handles TLS on the ESP32.
  • Pass the root CA certificate via setCACert() for proper validation.
  • Use setInsecure() for prototyping only. Ship with setCACert().
  • JSON POST is the standard pattern for sending sensor data to cloud APIs.

When something breaks

  • TLS handshake fails. Wrong or expired root CA. Update the PEM.
  • HTTP error 401. Wrong or missing API token. Check the auth header.
  • HTTP error 403. Server is reachable but your request is rejected. Check the API docs for required parameters.
  • Connection times out. Server is slow or unreachable. Add a longer timeout: http.setTimeout(10000) for 10 seconds.

What to build next

  • The ESP32 MQTT tutorial is the alternative to HTTPS for IoT. MQTT is lighter on bandwidth and battery.
  • The book Production IoT with ESP32 covers AWS IoT, Google Cloud IoT, and Azure IoT Hub in depth.
  • The HTTPS with root CA rotation tutorial (planned) covers what happens when your CA expires every few years and how to ship updates.