API Migration Guide for ESPHome 2026.7

ESPHome 2026.1 introduced a new URL format for the REST API — and the old format will be removed in ESPHome 2026.7. This guide explains what changed, what you can do today, and what will break in 2026.7 if you don't migrate.

Devices running the current firmware (after 2026.1) already support both URL formats.
You can start using the new URLs today, and they will continue to work after 2026.7.
Only the old-format URLs will break when 2026.7 ships.

What Changed

ESPHome's local REST API previously identified entities using a sanitized "object ID" derived from
the entity name: all characters are lowercased and non-alphanumeric characters are replaced with
underscores. For example, an entity named Zone 1 was accessible via:

GET http://konnected.local/binary_sensor/zone_1

Starting with ESPHome 2026.1.3, entity URLs also accept the entity's display name directly:

GET http://konnected.local/binary_sensor/Zone%201

The underlying logic is straightforward: take the display name as-is and percent-encode any
characters that are not valid in a URL path segment. The entity name Zone 1 becomes Zone%201
because the space character encodes as %20.

The old object_id-based URLs are deprecated and will be removed in ESPHome 2026.7.

Two separate but related changes ship across versions:

  1. HTTP/REST URL paths — new display-name format added in 2026.1.3; old object_id format removed in 2026.7
  2. SSE event id field — new name_id field added in 2026.1.3; id permanently adopts new format in 2026.8

Timeline

MilestoneESPHome versionWhat happens
New URLs available2026.1.3New display-name URLs begin working alongside old URLs. SSE gains a new name_id field. Old URLs still work but log deprecation warnings.
Current version2026.4Both old and new URL formats accepted. Both id and name_id in SSE payloads.
Breaking: REST2026.7.0Old object_id REST URLs are removed. Requests to old URLs return 404.
Breaking: SSE2026.8.0SSE name_id field removed. id field permanently adopts the new slash/display-name format.

Recommendation: Migrate to the new URL format now. It works on all current devices (ESPHome 2026.1.3+) and is the only format that will work after 2026.7.


HTTP/REST URL Changes

URL Format Rules

ComponentBefore (ESPHome < 2026.7)After (ESPHome ≥ 2026.7)
DomainUnchanged (binary_sensor, cover, etc.)Unchanged
Entity identifierSanitized slug: lowercase, non-alphanumeric → _URL-encoded display name
Action segmentUnchanged (turn_on, open, press, etc.)Unchanged

Encoding rules for display names:

  • Space → %20
  • +%2B
  • %%25
  • Forward slash / is now prohibited in entity names (ESPHome validation error)

Alarm Panel / Alarm Panel Pro

Entityv2 URL (ESPHome < 2026.7)v3 URL (ESPHome ≥ 2026.7)
Zone 1/binary_sensor/zone_1/binary_sensor/Zone%201
Zone 2/binary_sensor/zone_2/binary_sensor/Zone%202
Zone 3/binary_sensor/zone_3/binary_sensor/Zone%203
Zone 4/binary_sensor/zone_4/binary_sensor/Zone%204
Zone 5/binary_sensor/zone_5/binary_sensor/Zone%205
Zone 6/binary_sensor/zone_6/binary_sensor/Zone%206
Zone 7 (Pro)/binary_sensor/zone_7/binary_sensor/Zone%207
Zone 8 (Pro)/binary_sensor/zone_8/binary_sensor/Zone%208
Zone 9 (Pro)/binary_sensor/zone_9/binary_sensor/Zone%209
Zone 10 (Pro)/binary_sensor/zone_10/binary_sensor/Zone%2010
Zone 11 (Pro)/binary_sensor/zone_11/binary_sensor/Zone%2011
Zone 12 (Pro)/binary_sensor/zone_12/binary_sensor/Zone%2012
Alarm 1 output/switch/alarm_1/switch/Alarm%201
Alarm 2 output/switch/alarm_2/switch/Alarm%202
Alarm 1 turn on/switch/alarm_1/turn_on/switch/Alarm%201/turn_on
Alarm 2 turn on/switch/alarm_2/turn_on/switch/Alarm%202/turn_on
Warning Beep state/light/warning_beep/light/Warning%20Beep
Warning Beep on/light/warning_beep/turn_on/light/Warning%20Beep/turn_on
Alarm system state/alarm_control_panel/konnected_alarm/alarm_control_panel/Konnected%20Alarm
Arm away/alarm_control_panel/konnected_alarm/arm_away/alarm_control_panel/Konnected%20Alarm/arm_away
WiFi Signal dBm/sensor/wifi_signal_rssi/sensor/WiFi%20Signal
WiFi Signal %/sensor/wifi_signal__/sensor/WiFi%20Signal%20%25
Uptime/sensor/uptime/sensor/Uptime
Device ID/text_sensor/device_id/text_sensor/Device%20ID
IP Address/text_sensor/ip_address/text_sensor/IP%20Address
Restart/button/restart/press/button/Restart/press

GDO blaQ (GDOv2-Q)

Entityv2 URL (ESPHome < 2026.7)v3 URL (ESPHome ≥ 2026.7)
Garage door state/cover/garage_door/cover/Garage%20Door
Open/cover/garage_door/open/cover/Garage%20Door/open
Close/cover/garage_door/close/cover/Garage%20Door/close
Stop/cover/garage_door/stop/cover/Garage%20Door/stop
Toggle/cover/garage_door/toggle/cover/Garage%20Door/toggle
Set position/cover/garage_door/set?position=0.5/cover/Garage%20Door/set?position=0.5
Garage light/light/garage_light/light/Garage%20Light
Remote lock state/lock/lock/lock/Lock
Lock remotes/lock/lock/lock/lock/Lock/lock
Unlock remotes/lock/lock/unlock/lock/Lock/unlock
Motion sensor/binary_sensor/motion/binary_sensor/Motion
Obstruction sensor/binary_sensor/obstruction/binary_sensor/Obstruction
Motor running/binary_sensor/motor/binary_sensor/Motor
Wall button/binary_sensor/wall_button/binary_sensor/Wall%20Button
Protocol synced/binary_sensor/synced/binary_sensor/Synced
Garage openings/sensor/garage_openings/sensor/Garage%20Openings
Protocol setting/select/security__protocol/select/Security%2B%20protocol
Set protocol/select/security__protocol/set/select/Security%2B%20protocol/set
Learn mode/switch/learn/switch/Learn
Pre-close warning/button/pre_close_warning/press/button/Pre-close%20Warning/press
Play sound/button/play_sound/press/button/Play%20sound/press
Restart/button/restart/press/button/Restart/press
Factory Reset/button/factory_reset/press/button/Factory%20Reset/press

GDO White (GDOv2-S)

Entityv2 URL (ESPHome < 2026.7)v3 URL (ESPHome ≥ 2026.7)
Garage door state/cover/garage_door/cover/Garage%20Door
Open / Close / Stop / Toggle/cover/garage_door/{action}/cover/Garage%20Door/{action}
Wired sensor/binary_sensor/wired_sensor/binary_sensor/Wired%20Sensor
Range sensor state/binary_sensor/garage_door_range_sensor/binary_sensor/Garage%20Door%20Range%20Sensor
STR output state/switch/str_output/switch/STR%20output
STR turn on/switch/str_output/turn_on/switch/STR%20output/turn_on
Sensor distance/sensor/sensor_distance/sensor/Sensor%20distance
Calibration value/number/sensor_calibration/number/Sensor%20calibration
Set calibration/number/sensor_calibration/set?value=2.4/number/Sensor%20calibration/set?value=2.4
Pre-close warning/button/pre_close_warning/press/button/Pre-close%20Warning/press
Play sound/button/play_sound/press/button/Play%20sound/press
Restart/button/restart/press/button/Restart/press

SSE Event ID Changes

In addition to the REST URL changes, the entity identifier in Server-Sent Events payloads is also
changing. See the full SSE Events Reference for complete documentation. Here is
a summary of the id field evolution:

Before (ESPHome ≤ 2026.1.2)

data: {"id":"binary-sensor-zone_1","state":"ON","value":true}

The id format is "{domain}-{object_id}" where the domain uses hyphens (e.g., binary-sensor)
and the object_id is the sanitized lowercase slug.

During transition (ESPHome 2026.1.3 – 2026.7.x)

data: {"id":"binary-sensor-zone_1","name_id":"binary_sensor/Zone 1","state":"ON","value":true}

Both id (legacy format) and name_id (new format) are present simultaneously. The
legacy id continues to work for backward compatibility. Clients should begin using name_id
for forward compatibility.

After (ESPHome ≥ 2026.8.0)

data: {"id":"binary_sensor/Zone 1","state":"ON","value":true}

The name_id field is removed. The id field permanently adopts the new format:
"{domain}/{Entity Name}" where the domain uses underscores and the entity name is the display
name verbatim (not URL-encoded; spaces appear as literal spaces in the JSON value).

SSE id Format Reference

DomainOld id prefixNew id / name_id prefix
binary_sensorbinary-sensor-binary_sensor/
sensorsensor-sensor/
text_sensortext-sensor-text_sensor/
switchswitch-switch/
lightlight-light/
covercover-cover/
locklock-lock/
selectselect-select/
numbernumber-number/
buttonbutton-button/
alarm_control_panelalarm-control-panel-alarm_control_panel/

Why Entity Names Are Not Stable

Both REST URLs and SSE ids are derived deterministically from the entity's display name as
defined in the firmware at compile time. This means:

  • If a user or developer renames an entity in their firmware configuration and reflashes the
    device, all URL paths and SSE ids for that entity will change.
  • There is no stable opaque identifier. The display name is the identifier.
  • The default Konnected firmware uses consistent names (documented in the API spec), so for
    stock firmware builds this is predictable. Custom firmware may differ.

Recommendation: Instead of hardcoding URL paths, use the SSE-based endpoint discovery pattern
described in Endpoint Discovery Pattern. This approach reads
entity identifiers directly from the device at runtime and handles both URL format versions
transparently.


Migration Strategies

Strategy 1: Direct update (simplest)

Replace all hardcoded old-format URLs with new-format URLs using the tables above. Update
any SSE id parsing to handle both name_id (if present) and id.

This works well if:

  • You control the firmware and know entities won't be renamed
  • You only need to support devices running a single ESPHome version

Detecting the version at runtime:

GET http://konnected.local/text_sensor/ESPHome%20Version   (v3 devices)
GET http://konnected.local/text_sensor/esphome_version      (v2 devices)

If the v3 endpoint returns 200, the device is running 2026.7+. If it returns 404, fall back to
the v2 endpoint. Parse the version string to determine which URL format to use.

import requests

def get_esphome_version(host):
    # Try v3 URL first
    r = requests.get(f"http://{host}/text_sensor/ESPHome%20Version", timeout=5)
    if r.status_code == 200:
        return r.json()["state"]
    # Fall back to v2 URL
    r = requests.get(f"http://{host}/text_sensor/esphome_version", timeout=5)
    if r.status_code == 200:
        return r.json()["state"]
    return None

version = get_esphome_version("konnected.local")
use_v3_urls = version and tuple(int(x) for x in version.split(".")[:2]) >= (2026, 7)

Strategy 2: Try-new-then-fallback

Attempt the v3 URL first; if it returns 404, retry with the v2 URL. This makes the client
transparently compatible with both device generations without needing to check the version first.

def get_entity(host, domain, v3_name, v2_slug):
    """
    Fetch entity state, trying v3 URL first and falling back to v2.
    """
    from urllib.parse import quote
    v3_url = f"http://{host}/{domain}/{quote(v3_name)}"
    r = requests.get(v3_url, timeout=5)
    if r.status_code == 200:
        return r.json()
    # Fall back to v2 object_id slug
    v2_url = f"http://{host}/{domain}/{v2_slug}"
    return requests.get(v2_url, timeout=5).json()

# Example
state = get_entity("konnected.local", "binary_sensor", "Zone 1", "zone_1")

Strategy 3: Runtime endpoint discovery (recommended for integrations)

Connect to the SSE stream, discover entity identifiers from the initial state burst, and use
those identifiers to construct REST URLs. This approach is fully version-transparent and handles
custom entity names.

See the complete Endpoint Discovery Pattern guide.


curl Examples

Alarm Panel Pro — Zone 1 state

# v2 (ESPHome < 2026.7)
curl http://konnected.local/binary_sensor/zone_1

# v3 (ESPHome ≥ 2026.7)
curl "http://konnected.local/binary_sensor/Zone%201"

Alarm Panel Pro — Trigger Alarm 1

# v2
curl -X POST http://konnected.local/switch/alarm_1/turn_on

# v3
curl -X POST "http://konnected.local/switch/Alarm%201/turn_on"

GDO blaQ — Close garage door

# v2
curl -X POST http://konnected.local/cover/garage_door/close

# v3
curl -X POST "http://konnected.local/cover/Garage%20Door/close"

GDO blaQ — Set Security+ protocol

# v2
curl -X POST "http://konnected.local/select/security__protocol/set?option=auto"

# v3
curl -X POST "http://konnected.local/select/Security%2B%20protocol/set?option=auto"

GDO White — Read laser sensor distance

# v2
curl http://konnected.local/sensor/sensor_distance

# v3
curl "http://konnected.local/sensor/Sensor%20distance"

JavaScript Examples

const BASE = "http://konnected.local";

// Fetch zone state (v3)
async function getZoneState(zone) {
  const path = encodeURIComponent(`Zone ${zone}`);
  const r = await fetch(`${BASE}/binary_sensor/${path}`);
  return r.json();
}

// Open garage door (v3)
async function openGarageDoor() {
  await fetch(`${BASE}/cover/${encodeURIComponent("Garage Door")}/open`, {
    method: "POST"
  });
}

// Compatible SSE handler (reads name_id when present, falls back to id)
function connectSSE(onEvent) {
  const evtSource = new EventSource(`${BASE}/events`);
  evtSource.onmessage = (e) => {
    const data = JSON.parse(e.data);
    const entityId = data.name_id ?? data.id;
    onEvent(entityId, data.state, data.value);
  };
  return evtSource;
}

const BASE = "http://konnected.local";

// Fetch zone state (v3)
async function getZoneState(zone) {
  const path = encodeURIComponent(`Zone ${zone}`);
  const r = await fetch(`${BASE}/binary_sensor/${path}`);
  return r.json();
}

// Open garage door (v3)
async function openGarageDoor() {
  await fetch(`${BASE}/cover/${encodeURIComponent("Garage Door")}/open`, {
    method: "POST"
  });
}

// Compatible SSE handler (reads name_id when present, falls back to id)
function connectSSE(onEvent) {
  const evtSource = new EventSource(`${BASE}/events`);
  evtSource.onmessage = (e) => {
    const data = JSON.parse(e.data);
    const entityId = data.name_id ?? data.id;
    onEvent(entityId, data.state, data.value);
  };
  return evtSource;
}

Related Resources