¿Cuántas veces tu ESP32 ha despertado tras un corte de luz y ha quedado con la hora en cero? El RTC interno se reinicia, el cliente NTP a veces falla con el firewall del router, y de repente tu datalogger marca timestamps inventados. Una llamada HTTP GET a una API pública de tiempo arregla el problema en menos de 200 líneas de C: el ESP32 sabe su hora real, su zona horaria y si hay horario de verano activo, sin librerías extra ni servicios de pago.

Esta guía te lleva desde un proyecto vacío en ESP IDF hasta un ESP32 que cada 60 segundos consulta TimeAPI.io, parsea el JSON sin librerías externas, y deja la hora lista para que la uses en un display, un log o una decisión de control. Al final vas a entender por qué esp_crt_bundle es lo que hace funcionar HTTPS en el ESP32, cómo manejar la respuesta por chunks con un event handler, y cómo extender el proyecto con un OLED SSD1306 para mostrar la hora sin computador.

¿Por qué HTTP GET y no NTP?

NTP es el estándar para sincronizar relojes, pero tiene tres dolores en proyectos IoT pequeños:

  • Usa UDP, que muchos firewalls corporativos y enrutadores móviles bloquean.
  • Te entrega un timestamp Unix puro; tú tienes que resolver zona horaria y DST manualmente.
  • La librería de SNTP en ESP IDF requiere configurar servidores, esperar sincronización, y manejar fallos de pool.

Una llamada HTTP GET a TimeAPI.io devuelve un JSON con fecha, hora, zona horaria y flag dstActive ya calculados por el servidor. HTTPS pasa por casi todos los firewalls. Y el parsing es trivial.

La contrapartida: NTP es más preciso (latencia compensada) y no depende de un servicio público. Si necesitas precisión sub segundo, NTP. Si quieres simplicidad y zona horaria resuelta, HTTP GET a una time API.

Anatomía de una petición HTTP, en concreto

Una petición HTTP tiene tres partes que vas a tocar de forma explícita o implícita en el código del ESP32:

  1. Request line: método (GET, POST, PUT, DELETE), recurso (/api/time/current/zone) y versión (HTTP/1.1).
  2. Headers: metadatos como Host, User-Agent, Accept. El cliente HTTP del ESP IDF los completa solo.
  3. Body: opcional, solo en POST y PUT. En un GET típicamente está vacío.

El servidor responde con un código (200 OK, 404 Not Found, 5xx error) y un cuerpo que en nuestro caso es el JSON.

Endpoint que vamos a consumir:

Código
https://timeapi.io/api/Time/current/zone?timeZone=America/Santiago

Reemplaza America/Santiago por tu zona si estás en otro lado. La respuesta es:

JSON
{
  "year": 2026,
  "month": 6,
  "day": 20,
  "hour": 16,
  "minute": 24,
  "seconds": 12,
  "milliSeconds": 7,
  "dateTime": "2026-06-20T16:24:12.0076021",
  "date": "06/20/2026",
  "time": "16:24",
  "timeZone": "America/Santiago",
  "dayOfWeek": "Saturday",
  "dstActive": false
}

ESP32 HTTP GET request a TimeAPI con respuesta JSON

Lo que necesitas

  • Placa ESP32 DevKit (cualquier variante: ESP32 clásico, ESP32-S3, ESP32-C3, ESP32-C6). Yo voy a usar un ESP32-S3 por defecto, pero el código es portable.
  • Cable USB-C de datos (importante: muchos cables baratos son solo de carga; si la placa no aparece como COM, prueba otro cable).
  • VS Code con la extensión ESP IDF de Espressif instalada. Si nunca la configuraste, sigue primero la guía de instalación de ESP IDF en VS Code.
  • WiFi 2.4 GHz con acceso a internet (el ESP32 clásico y muchas variantes solo soportan 2.4 GHz).

Crear el proyecto desde el wizard de ESP IDF

La extensión de ESP IDF en VS Code trae un asistente que genera todo el scaffolding por ti.

  1. Abre la barra lateral de Espressif IDF.
  2. Expande Advanced y elige New Project Wizard.
  3. Selecciona Use ESP IDF v5.4.1 (o la versión instalada).
  4. Completa los campos:
    • Project Name: por ejemplo, esp32-timeapi.
    • Enter Project Directory: una carpeta local. Nunca uses Google Drive / OneDrive / Dropbox. el build escribe miles de archivos y la sincronización tumba el proceso.
    • ESP IDF Target: el chip que tienes (esp32, esp32s3, esp32c3, etc.).
    • ESP IDF Board: si es un S3 vía USB JTAG built in, elige esa opción.
    • Serial Port: el COM port en el que aparece tu placa.
  5. Choose TemplateESP IDF Templatessample_projectCreate project using template.

Wizard de nuevo proyecto en la extensión ESP IDF de VS Code

Después de un par de segundos VS Code te ofrece abrir el proyecto nuevo. Si no aparece la notificación, File → Open Folder… y selecciona la carpeta del proyecto.

Selección de directorio y chip target en el wizard

El wizard crea muchos archivos; para esta guía solo vas a tocar main/main.c.

Código completo

Pega esto en main/main.c, reemplazando el hello_world que viene por defecto. Cambia tu SSID, password y zona horaria.

C
/*
    Rui Santos & Sara Santos - Random Nerd Tutorials
    https://RandomNerdTutorials.com/esp-idf-esp32-http-get/
*/
#include <string.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "esp_log.h"
#include "esp_wifi.h"
#include "esp_event.h"
#include "nvs_flash.h"
#include "esp_http_client.h"
#include "esp_crt_bundle.h"

// REPLACE WITH YOUR SSID AND PASSWORD
#define WIFI_SSID   "REPLACE_WITH_YOUR_SSID"
#define WIFI_PASS   "REPLACE_WITH_YOUR_PASSWORD"

// REPLACE WITH YOUR TIME ZONE - https://timeapi.io/api/TimeZone/AvailableTimeZones
#define TIME_ZONE   "Europe/Lisbon"

// TimeAPI.io endpoint with timezone query
#define API_URL     "https://timeapi.io/api/time/current/zone?timeZone=" TIME_ZONE

// Tag for logging
static const char *TAG = "http_get";

// Buffer for HTTP response
#define BUF_SIZE    512
static char response_buf[BUF_SIZE];
static int  response_len = 0;

// HTTP event handler to collect response data from the request
static esp_err_t http_event_handler(esp_http_client_event_t *evt)
{
    if (evt->event_id == HTTP_EVENT_ON_DATA) {
        int copy = evt->data_len;
        if (response_len + copy >= BUF_SIZE) copy = BUF_SIZE - response_len - 1;
        memcpy(response_buf + response_len, evt->data, copy);
        response_len += copy;
        response_buf[response_len] = '\0';
    }
    return ESP_OK;
}

// Function that extracts a JSON field (no library needed)
// Finds "key":"value" or "key":value and copies the value
static int get_json_value(const char *json, const char *key, char *out, int out_size)
{
    char search[64];
    snprintf(search, sizeof(search), "\"%s\":", key);
    const char *p = strstr(json, search);
    if (!p) return 0;
    p += strlen(search);
    while (*p == ' ') p++;
    int is_string = (*p == '"');
    if (is_string) p++;
    int i = 0;
    while (*p && i < out_size - 1) {
        if (is_string && *p == '"') break;
        if (!is_string && (*p == ',' || *p == '}')) break;
        out[i++] = *p++;
    }
    out[i] = '\0';
    return i;
}

// Function that makes an HTTP GET request to the API, parses the JSON response, and prints the date and time
static void get_and_print_time(void)
{
    // Reset response buffer
    response_len = 0;
    memset(response_buf, 0, BUF_SIZE);

    // Configure HTTP client
    esp_http_client_config_t config = {
        .url               = API_URL,
        .event_handler     = http_event_handler,
        .crt_bundle_attach = esp_crt_bundle_attach,
    };

    // Initialize HTTP client and perform the HTTP GET request
    esp_http_client_handle_t client = esp_http_client_init(&config);
    esp_err_t err = esp_http_client_perform(client);

    // Check if the request was successful (ESP_OK and status code 200) and print the results
    if (err == ESP_OK) {
        int status = esp_http_client_get_status_code(client);
        if (status == 200) {
            // Extract fields (values) from the JSON response
            char date[20], time[20], day_of_week[16], tz[40];
            get_json_value(response_buf, "date",      date,        sizeof(date));
            get_json_value(response_buf, "time",      time,        sizeof(time));
            get_json_value(response_buf, "dayOfWeek", day_of_week, sizeof(day_of_week));
            get_json_value(response_buf, "timeZone",  tz,          sizeof(tz));
            // Print the results
            ESP_LOGI(TAG, "------------------------------------");
            ESP_LOGI(TAG, "Timezone: %s", tz);
            ESP_LOGI(TAG, "Date    : %s (%s)", date, day_of_week);
            ESP_LOGI(TAG, "Time    : %s", time);
            ESP_LOGI(TAG, "------------------------------------");
        } else {
            ESP_LOGW(TAG, "HTTP status %d", status);
        }
    } else {
        ESP_LOGE(TAG, "HTTP request failed: %s", esp_err_to_name(err));
    }
    // Cleanup HTTP client
    esp_http_client_cleanup(client);
}

// Event group to signal when connected to Wi-Fi
static EventGroupHandle_t s_wifi_event_group;
#define WIFI_CONNECTED_BIT BIT0

// Wi-Fi and IP event handler
static void wifi_event_handler(void *arg, esp_event_base_t base,
                               int32_t id, void *data)
{
    if (base == WIFI_EVENT && id == WIFI_EVENT_STA_START) {
        ESP_LOGI(TAG, "Wi-Fi STA started. Connecting to %s...", WIFI_SSID);
        esp_wifi_connect();
    } else if (base == WIFI_EVENT && id == WIFI_EVENT_STA_DISCONNECTED) {
        ESP_LOGW(TAG, "Wi-Fi disconnected. Retrying connection...");
        esp_wifi_connect();
    } else if (base == IP_EVENT && id == IP_EVENT_STA_GOT_IP) {
        ip_event_got_ip_t *e = (ip_event_got_ip_t *)data;
        ESP_LOGI(TAG, "Got IP Address: " IPSTR, IP2STR(&e->ip_info.ip));
        xEventGroupSetBits(s_wifi_event_group, WIFI_CONNECTED_BIT);
    }
}

// Initializes Wi-Fi in station mode and waits for connection
static void wifi_init(void)
{
    // Create event group to signal when connected
    s_wifi_event_group = xEventGroupCreate();

    // Initialize TCP/IP stack and event loop
    ESP_ERROR_CHECK(esp_netif_init());
    ESP_ERROR_CHECK(esp_event_loop_create_default());

    // Create default Wi-Fi STA interface
    esp_netif_create_default_wifi_sta();
    // Initialize Wi-Fi
    wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT();
    ESP_ERROR_CHECK(esp_wifi_init(&cfg));

    // Register event handlers
    esp_event_handler_instance_register(WIFI_EVENT, ESP_EVENT_ANY_ID,
                                        wifi_event_handler, NULL, NULL);
    esp_event_handler_instance_register(IP_EVENT, IP_EVENT_STA_GOT_IP,
                                        wifi_event_handler, NULL, NULL);

    // Configure Wi-Fi STA
    wifi_config_t wifi_cfg = {
        .sta = {
            .ssid     = WIFI_SSID,
            .password = WIFI_PASS,
        },
    };
    ESP_ERROR_CHECK(esp_wifi_set_mode(WIFI_MODE_STA));
    ESP_ERROR_CHECK(esp_wifi_set_config(WIFI_IF_STA, &wifi_cfg));
    ESP_ERROR_CHECK(esp_wifi_start());

    ESP_LOGI(TAG, "Connecting to Wi-Fi...");

    // Wait until connected
    xEventGroupWaitBits(s_wifi_event_group, WIFI_CONNECTED_BIT,
                        pdFALSE, pdTRUE, portMAX_DELAY);
}

// Task that periodically makes an HTTP GET request to the API and prints the time
static void http_get_task(void *arg)
{
    while (true) {
        get_and_print_time();
        // Wait for 60 seconds before the next request
        vTaskDelay(pdMS_TO_TICKS(60000));
    }
}

void app_main(void)
{
    // Initialize NVS
    esp_err_t ret = nvs_flash_init();
    if (ret == ESP_ERR_NVS_NO_FREE_PAGES || ret == ESP_ERR_NVS_NEW_VERSION_FOUND) {
        ESP_ERROR_CHECK(nvs_flash_erase());
        ret = nvs_flash_init();
    }
    ESP_ERROR_CHECK(ret);

    // Initialize Wi-Fi
    wifi_init();

    // Start the HTTP GET time fetching task
    xTaskCreate(http_get_task, "http_get_task", 8192, NULL, 5, NULL);
}

Para Chile, reemplaza:

C
#define TIME_ZONE   "America/Santiago"

Cómo funciona el código por dentro

Las librerías que importan

Las clave son tres:

  • esp_http_client.h te da un cliente HTTP/HTTPS de alto nivel. Encapsula la negociación TLS, las conexiones, los timeouts y el chunked transfer encoding. Sin esta librería, tendrías que usar mbedtls directamente, que son varias páginas de boilerplate.
  • esp_crt_bundle.h es lo que hace que HTTPS funcione "de una". Espressif empaqueta los certificados raíz de las autoridades públicas (Let's Encrypt, DigiCert, etc.) dentro del firmware. Cuando configuras crt_bundle_attach = esp_crt_bundle_attach, el cliente valida el certificado del servidor contra ese bundle sin que tengas que copiar .pem manualmente.
  • nvs_flash.h persiste la configuración de WiFi en flash. Si quitas esto, cada reinicio pierde las credenciales.

El truco del response_buf y el event handler

esp_http_client_perform() es síncrono pero el cuerpo de la respuesta llega en chunks vía callback. La razón es que TCP no te entrega "todo el cuerpo de golpe". llega en fragmentos según el MTU. El handler http_event_handler() captura el evento HTTP_EVENT_ON_DATA y va concatenando los pedazos en response_buf. Por eso es global: el callback puede dispararse varias veces antes de que el perform() retorne.

El límite de 512 bytes es suficiente para TimeAPI (la respuesta JSON es ~300 bytes), pero si vas a consumir una API que devuelve más datos, sube BUF_SIZE y revisa el guard response_len + copy >= BUF_SIZE que previene buffer overflow.

Parser JSON sin librerías

get_json_value() es un parser minimalista: busca "key": con strstr, salta espacios, detecta si el valor es string (comilla) o número, y copia hasta el delimitador (", ,, }). No maneja JSON anidado ni arrays. perfecto para respuestas planas como esta, totalmente insuficiente para APIs complejas.

Si vas a hablar con APIs más serias, cambia este parser por cJSON (incluida en ESP IDF):

C
#include "cJSON.h"

cJSON *root = cJSON_Parse(response_buf);
const char *date = cJSON_GetObjectItem(root, "date")->valuestring;
ESP_LOGI(TAG, "Date: %s", date);
cJSON_Delete(root);

El loop de 60 segundos

http_get_task() corre en una FreeRTOS task aparte (8 KB de stack). vTaskDelay(pdMS_TO_TICKS(60000)) libera la CPU durante un minuto entre requests. Si pones delay(60000) o sleep(60) en su lugar, bloqueas el scheduler y el WiFi se cae.

Build, flash y monitor

En la barra inferior de VS Code, los íconos de la extensión ESP IDF resuelven todo el flujo:

  1. Ícono de estrella → método de flash: UART.
  2. Ícono de enchufe → COM port donde aparece tu ESP32.
  3. Ícono de chip → target device (esp32, esp32s3, etc.).
  4. Ícono de llave inglesaBuild Project. La primera build tarda varios minutos.
  5. Ícono de rayoFlash Device. En algunas placas hay que mantener presionado el botón BOOT mientras parte el flasheo.
  6. Ícono de pantallaMonitor Device para ver la salida serial.

Build del proyecto ESP IDF en VS Code con mensaje Success

Salida esperada cada 60 segundos:

Código
I (12345) http_get: ------------------------------------
I (12345) http_get: Timezone: America/Santiago
I (12345) http_get: Date    : 06/20/2026 (Saturday)
I (12345) http_get: Time    : 16:24
I (12345) http_get: ------------------------------------

Variantes y mejoras

El proyecto base imprime al monitor serial. perfecto para debugging, inútil cuando el ESP32 va a un proyecto definitivo. Estas extensiones tienen impacto real:

  • OLED SSD1306 mostrando hora, zona y DST. Conectas el SSD1306 (I²C, GPIO 21 SDA / GPIO 22 SCL en ESP32 clásico, o GPIO 8/9 en S3) y reemplazas las líneas ESP_LOGI por escritura en pantalla. Componente esencial 0.96" o 1.3". pasas de "imprime al monitor" a "reloj autónomo en escritorio" con menos de 50 líneas adicionales. La librería u8g2 portada a ESP IDF es la más limpia.
  • Cache de la última hora en NVS para sobrevivir cortes de internet. Cada respuesta exitosa la guardas en NVS; al boot, mientras no haya WiFi, el ESP32 usa la última hora conocida + millis() para estimar la actual. No es preciso, pero evita timestamps en cero.
  • Sincronización adaptativa según error de drift. En vez de pegarle a la API cada 60 s, mide el drift entre el reloj interno y la respuesta de la API: si tu RTC está dentro de ±1 segundo, sube el intervalo a 10 minutos; si se desvía más, baja a 30 segundos. Reduce el tráfico HTTP y el consumo en proyectos con batería.
  • Fallback con cJSON + lista de time APIs. Si TimeAPI.io no responde, intentas worldtimeapi.org. Listas dos endpoints y reintenta el handler. Robustez sin gran complejidad.

Personalización para Chile

Componentes del catálogo MechatronicStore que cubren este tutorial:

  • Placa ESP32 DevKit (o ESP32-S3 si quieres más Flash/PSRAM). núcleo del proyecto.
  • Cable USB-C datos. flasheo y comunicación serial.
  • Pantalla OLED SSD1306 0.96" I²C. solo necesaria si haces la extensión OLED. Sin esto, el ESP32 imprime al monitor serial y no requiere display.

Para la zona horaria local, usa "America/Santiago" en el #define TIME_ZONE. El campo dstActive de TimeAPI cubre el cambio de hora chileno automáticamente.

Nota: en esta corrida el detector automático de catálogo no pudo confirmar SKUs ni stock en vivo. Verifica disponibilidad y precio actual directamente en mechatronicstore.cl antes de hacer el pedido.

Recursos

Versión chilena inspirada en el tutorial de Rui Santos & Sara Santos (Random Nerd Tutorials), con zona horaria local y extensiones que el original no cubre.