diff --git a/app/boards/arm/corneish_zen/widgets/output_status.c b/app/boards/arm/corneish_zen/widgets/output_status.c
index ad0c2b1a..bdf90cc3 100644
--- a/app/boards/arm/corneish_zen/widgets/output_status.c
+++ b/app/boards/arm/corneish_zen/widgets/output_status.c
@@ -14,9 +14,8 @@ LOG_MODULE_DECLARE(zmk, CONFIG_ZMK_LOG_LEVEL);
 #include <zmk/display.h>
 #include "output_status.h"
 #include <zmk/event_manager.h>
-#include <zmk/events/usb_conn_state_changed.h>
 #include <zmk/events/ble_active_profile_changed.h>
-#include <zmk/events/endpoint_selection_changed.h>
+#include <zmk/events/endpoint_changed.h>
 #include <zmk/usb.h>
 #include <zmk/ble.h>
 #include <zmk/endpoints.h>
@@ -39,31 +38,31 @@ LV_IMG_DECLARE(USB_connected);
 static sys_slist_t widgets = SYS_SLIST_STATIC_INIT(&widgets);
 
 struct output_status_state {
-    enum zmk_endpoint selected_endpoint;
+    struct zmk_endpoint_instance selected_endpoint;
     bool active_profile_connected;
     bool active_profile_bonded;
     uint8_t active_profile_index;
 };
 
 static struct output_status_state get_state(const zmk_event_t *_eh) {
-    return (struct output_status_state){.selected_endpoint = zmk_endpoints_selected(),
-                                        .active_profile_connected =
-                                            zmk_ble_active_profile_is_connected(),
-                                        .active_profile_bonded = !zmk_ble_active_profile_is_open(),
-                                        .active_profile_index = zmk_ble_active_profile_index()};
+    return (struct output_status_state){
+        .selected_endpoint = zmk_endpoints_selected(),
+        .active_profile_connected = zmk_ble_active_profile_is_connected(),
+        .active_profile_bonded = !zmk_ble_active_profile_is_open(),
+    };
     ;
 }
 
 static void set_status_symbol(lv_obj_t *icon, struct output_status_state state) {
-    switch (state.selected_endpoint) {
-    case ZMK_ENDPOINT_USB:
+    switch (state.selected_endpoint.transport) {
+    case ZMK_TRANSPORT_USB:
         lv_img_set_src(icon, &USB_connected);
         break;
-    case ZMK_ENDPOINT_BLE:
+    case ZMK_TRANSPORT_BLE:
         if (state.active_profile_bonded) {
             if (state.active_profile_connected) {
                 // sprintf(text, LV_SYMBOL_BLUETOOTH "%i " LV_SYMBOL_OK, active_profile_index);
-                switch (state.active_profile_index) {
+                switch (state.selected_endpoint.ble.profile_index) {
                 case 0:
                     lv_img_set_src(icon, &bluetooth_connected_1);
                     break;
@@ -84,7 +83,7 @@ static void set_status_symbol(lv_obj_t *icon, struct output_status_state state)
                 lv_img_set_src(icon, &bluetooth_disconnected_right);
             }
         } else {
-            switch (state.active_profile_index) {
+            switch (state.selected_endpoint.ble.profile_index) {
             case 0:
                 lv_img_set_src(icon, &bluetooth_advertising_1);
                 break;
@@ -113,11 +112,9 @@ static void output_status_update_cb(struct output_status_state state) {
 
 ZMK_DISPLAY_WIDGET_LISTENER(widget_output_status, struct output_status_state,
                             output_status_update_cb, get_state)
-ZMK_SUBSCRIPTION(widget_output_status, zmk_endpoint_selection_changed);
-
-#if IS_ENABLED(CONFIG_USB_DEVICE_STACK)
-ZMK_SUBSCRIPTION(widget_output_status, zmk_usb_conn_state_changed);
-#endif
+ZMK_SUBSCRIPTION(widget_output_status, zmk_endpoint_changed);
+// We don't get an endpoint changed event when the active profile connects/disconnects
+// but there wasn't another endpoint to switch from/to, so update on BLE events too.
 #if defined(CONFIG_ZMK_BLE)
 ZMK_SUBSCRIPTION(widget_output_status, zmk_ble_active_profile_changed);
 #endif
diff --git a/app/include/zmk/endpoints.h b/app/include/zmk/endpoints.h
index c8860533..c5964ff8 100644
--- a/app/include/zmk/endpoints.h
+++ b/app/include/zmk/endpoints.h
@@ -6,10 +6,66 @@
 
 #pragma once
 
+#include <zmk/ble.h>
 #include <zmk/endpoints_types.h>
 
-int zmk_endpoints_select(enum zmk_endpoint endpoint);
-int zmk_endpoints_toggle();
-enum zmk_endpoint zmk_endpoints_selected();
+/**
+ * Recommended length of string buffer for printing endpoint identifiers.
+ */
+#define ZMK_ENDPOINT_STR_LEN 10
+
+#ifdef CONFIG_ZMK_USB
+#define ZMK_ENDPOINT_USB_COUNT 1
+#else
+#define ZMK_ENDPOINT_USB_COUNT 0
+#endif
+
+#ifdef CONFIG_ZMK_BLE
+#define ZMK_ENDPOINT_BLE_COUNT ZMK_BLE_PROFILE_COUNT
+#else
+#define ZMK_ENDPOINT_BLE_COUNT 0
+#endif
+
+/**
+ * The total number of different (struct zmk_endpoint_instance) values that can
+ * be selected.
+ *
+ * Note that this value may change between firmware versions, so it should not
+ * be used in any persistent storage.
+ */
+#define ZMK_ENDPOINT_COUNT (ZMK_ENDPOINT_USB_COUNT + ZMK_ENDPOINT_BLE_COUNT)
+
+bool zmk_endpoint_instance_eq(struct zmk_endpoint_instance a, struct zmk_endpoint_instance b);
+
+/**
+ * Writes a string identifying an endpoint instance.
+ *
+ * @param str Address of output string buffer
+ * @param len Length of string buffer. See ZMK_ENDPOINT_STR_LEN for recommended length.
+ *
+ * @returns Number of characters written.
+ */
+int zmk_endpoint_instance_to_str(struct zmk_endpoint_instance endpoint, char *str, size_t len);
+
+/**
+ * Gets a unique index for an endpoint instance. This can be used together with
+ * ZMK_ENDPOINT_COUNT to manage separate state for each endpoint instance.
+ *
+ * Note that the index for a specific instance may change between firmware versions,
+ * so it should not be used in any persistent storage.
+ */
+int zmk_endpoint_instance_to_index(struct zmk_endpoint_instance endpoint);
+
+/**
+ * Sets the preferred endpoint transport to use. (If the preferred endpoint is
+ * not available, a different one may automatically be selected.)
+ */
+int zmk_endpoints_select_transport(enum zmk_transport transport);
+int zmk_endpoints_toggle_transport(void);
+
+/**
+ * Gets the currently-selected endpoint.
+ */
+struct zmk_endpoint_instance zmk_endpoints_selected(void);
 
 int zmk_endpoints_send_report(uint16_t usage_page);
diff --git a/app/include/zmk/endpoints_types.h b/app/include/zmk/endpoints_types.h
index 70804a61..ea51c8aa 100644
--- a/app/include/zmk/endpoints_types.h
+++ b/app/include/zmk/endpoints_types.h
@@ -6,7 +6,33 @@
 
 #pragma once
 
-enum zmk_endpoint {
-    ZMK_ENDPOINT_USB,
-    ZMK_ENDPOINT_BLE,
+/**
+ * The method by which data is sent.
+ */
+enum zmk_transport {
+    ZMK_TRANSPORT_USB,
+    ZMK_TRANSPORT_BLE,
+};
+
+/**
+ * Configuration to select an endpoint on ZMK_TRANSPORT_USB.
+ */
+struct zmk_transport_usb_data {};
+
+/**
+ * Configuration to select an endpoint on ZMK_TRANSPORT_BLE.
+ */
+struct zmk_transport_ble_data {
+    int profile_index;
+};
+
+/**
+ * A specific endpoint to which data may be sent.
+ */
+struct zmk_endpoint_instance {
+    enum zmk_transport transport;
+    union {
+        struct zmk_transport_usb_data usb; // ZMK_TRANSPORT_USB
+        struct zmk_transport_ble_data ble; // ZMK_TRANSPORT_BLE
+    };
 };
diff --git a/app/include/zmk/events/endpoint_selection_changed.h b/app/include/zmk/events/endpoint_changed.h
similarity index 61%
rename from app/include/zmk/events/endpoint_selection_changed.h
rename to app/include/zmk/events/endpoint_changed.h
index 198fe5a1..2147009b 100644
--- a/app/include/zmk/events/endpoint_selection_changed.h
+++ b/app/include/zmk/events/endpoint_changed.h
@@ -11,8 +11,8 @@
 #include <zmk/endpoints_types.h>
 #include <zmk/event_manager.h>
 
-struct zmk_endpoint_selection_changed {
-    enum zmk_endpoint endpoint;
+struct zmk_endpoint_changed {
+    struct zmk_endpoint_instance endpoint;
 };
 
-ZMK_EVENT_DECLARE(zmk_endpoint_selection_changed);
+ZMK_EVENT_DECLARE(zmk_endpoint_changed);
diff --git a/app/src/behaviors/behavior_outputs.c b/app/src/behaviors/behavior_outputs.c
index 7aab8ee3..6ae81a0f 100644
--- a/app/src/behaviors/behavior_outputs.c
+++ b/app/src/behaviors/behavior_outputs.c
@@ -24,11 +24,11 @@ static int on_keymap_binding_pressed(struct zmk_behavior_binding *binding,
                                      struct zmk_behavior_binding_event event) {
     switch (binding->param1) {
     case OUT_TOG:
-        return zmk_endpoints_toggle();
+        return zmk_endpoints_toggle_transport();
     case OUT_USB:
-        return zmk_endpoints_select(ZMK_ENDPOINT_USB);
+        return zmk_endpoints_select_transport(ZMK_TRANSPORT_USB);
     case OUT_BLE:
-        return zmk_endpoints_select(ZMK_ENDPOINT_BLE);
+        return zmk_endpoints_select_transport(ZMK_TRANSPORT_BLE);
     default:
         LOG_ERR("Unknown output command: %d", binding->param1);
     }
diff --git a/app/src/display/widgets/output_status.c b/app/src/display/widgets/output_status.c
index 1c6da4b9..da29a95f 100644
--- a/app/src/display/widgets/output_status.c
+++ b/app/src/display/widgets/output_status.c
@@ -12,9 +12,8 @@ LOG_MODULE_DECLARE(zmk, CONFIG_ZMK_LOG_LEVEL);
 #include <zmk/display.h>
 #include <zmk/display/widgets/output_status.h>
 #include <zmk/event_manager.h>
-#include <zmk/events/usb_conn_state_changed.h>
 #include <zmk/events/ble_active_profile_changed.h>
-#include <zmk/events/endpoint_selection_changed.h>
+#include <zmk/events/endpoint_changed.h>
 #include <zmk/usb.h>
 #include <zmk/ble.h>
 #include <zmk/endpoints.h>
@@ -22,40 +21,38 @@ LOG_MODULE_DECLARE(zmk, CONFIG_ZMK_LOG_LEVEL);
 static sys_slist_t widgets = SYS_SLIST_STATIC_INIT(&widgets);
 
 struct output_status_state {
-    enum zmk_endpoint selected_endpoint;
+    struct zmk_endpoint_instance selected_endpoint;
     bool active_profile_connected;
     bool active_profile_bonded;
-    uint8_t active_profile_index;
 };
 
 static struct output_status_state get_state(const zmk_event_t *_eh) {
     return (struct output_status_state){.selected_endpoint = zmk_endpoints_selected(),
                                         .active_profile_connected =
                                             zmk_ble_active_profile_is_connected(),
-                                        .active_profile_bonded = !zmk_ble_active_profile_is_open(),
-                                        .active_profile_index = zmk_ble_active_profile_index()};
+                                        .active_profile_bonded = !zmk_ble_active_profile_is_open()};
     ;
 }
 
 static void set_status_symbol(lv_obj_t *label, struct output_status_state state) {
     char text[10] = {};
 
-    switch (state.selected_endpoint) {
-    case ZMK_ENDPOINT_USB:
+    switch (state.selected_endpoint.transport) {
+    case ZMK_TRANSPORT_USB:
         strcat(text, LV_SYMBOL_USB);
         break;
-    case ZMK_ENDPOINT_BLE:
+    case ZMK_TRANSPORT_BLE:
         if (state.active_profile_bonded) {
             if (state.active_profile_connected) {
                 snprintf(text, sizeof(text), LV_SYMBOL_WIFI " %i " LV_SYMBOL_OK,
-                         state.active_profile_index + 1);
+                         state.selected_endpoint.ble.profile_index + 1);
             } else {
                 snprintf(text, sizeof(text), LV_SYMBOL_WIFI " %i " LV_SYMBOL_CLOSE,
-                         state.active_profile_index + 1);
+                         state.selected_endpoint.ble.profile_index + 1);
             }
         } else {
             snprintf(text, sizeof(text), LV_SYMBOL_WIFI " %i " LV_SYMBOL_SETTINGS,
-                     state.active_profile_index + 1);
+                     state.selected_endpoint.ble.profile_index + 1);
         }
         break;
     }
@@ -70,11 +67,9 @@ static void output_status_update_cb(struct output_status_state state) {
 
 ZMK_DISPLAY_WIDGET_LISTENER(widget_output_status, struct output_status_state,
                             output_status_update_cb, get_state)
-ZMK_SUBSCRIPTION(widget_output_status, zmk_endpoint_selection_changed);
-
-#if IS_ENABLED(CONFIG_USB_DEVICE_STACK)
-ZMK_SUBSCRIPTION(widget_output_status, zmk_usb_conn_state_changed);
-#endif
+ZMK_SUBSCRIPTION(widget_output_status, zmk_endpoint_changed);
+// We don't get an endpoint changed event when the active profile connects/disconnects
+// but there wasn't another endpoint to switch from/to, so update on BLE events too.
 #if defined(CONFIG_ZMK_BLE)
 ZMK_SUBSCRIPTION(widget_output_status, zmk_ble_active_profile_changed);
 #endif
diff --git a/app/src/endpoints.c b/app/src/endpoints.c
index dbd1a3e6..e208a36a 100644
--- a/app/src/endpoints.c
+++ b/app/src/endpoints.c
@@ -16,29 +16,29 @@
 #include <zmk/event_manager.h>
 #include <zmk/events/ble_active_profile_changed.h>
 #include <zmk/events/usb_conn_state_changed.h>
-#include <zmk/events/endpoint_selection_changed.h>
+#include <zmk/events/endpoint_changed.h>
 
 #include <zephyr/logging/log.h>
 LOG_MODULE_DECLARE(zmk, CONFIG_ZMK_LOG_LEVEL);
 
-#define DEFAULT_ENDPOINT                                                                           \
-    COND_CODE_1(IS_ENABLED(CONFIG_ZMK_BLE), (ZMK_ENDPOINT_BLE), (ZMK_ENDPOINT_USB))
+#define DEFAULT_TRANSPORT                                                                          \
+    COND_CODE_1(IS_ENABLED(CONFIG_ZMK_BLE), (ZMK_TRANSPORT_BLE), (ZMK_TRANSPORT_USB))
 
-static enum zmk_endpoint current_endpoint = DEFAULT_ENDPOINT;
-static enum zmk_endpoint preferred_endpoint =
-    ZMK_ENDPOINT_USB; /* Used if multiple endpoints are ready */
+static struct zmk_endpoint_instance current_instance = {};
+static enum zmk_transport preferred_transport =
+    ZMK_TRANSPORT_USB; /* Used if multiple endpoints are ready */
 
-static void update_current_endpoint();
+static void update_current_endpoint(void);
 
 #if IS_ENABLED(CONFIG_SETTINGS)
 static void endpoints_save_preferred_work(struct k_work *work) {
-    settings_save_one("endpoints/preferred", &preferred_endpoint, sizeof(preferred_endpoint));
+    settings_save_one("endpoints/preferred", &preferred_transport, sizeof(preferred_transport));
 }
 
 static struct k_work_delayable endpoints_save_work;
 #endif
 
-static int endpoints_save_preferred() {
+static int endpoints_save_preferred(void) {
 #if IS_ENABLED(CONFIG_SETTINGS)
     return k_work_reschedule(&endpoints_save_work, K_MSEC(CONFIG_ZMK_SETTINGS_SAVE_DEBOUNCE));
 #else
@@ -46,14 +46,60 @@ static int endpoints_save_preferred() {
 #endif
 }
 
-int zmk_endpoints_select(enum zmk_endpoint endpoint) {
-    LOG_DBG("Selected endpoint %d", endpoint);
+bool zmk_endpoint_instance_eq(struct zmk_endpoint_instance a, struct zmk_endpoint_instance b) {
+    if (a.transport != b.transport) {
+        return false;
+    }
 
-    if (preferred_endpoint == endpoint) {
+    switch (a.transport) {
+    case ZMK_TRANSPORT_USB:
+        return true;
+
+    case ZMK_TRANSPORT_BLE:
+        return a.ble.profile_index == b.ble.profile_index;
+    }
+
+    LOG_ERR("Invalid transport %d", a.transport);
+    return false;
+}
+
+int zmk_endpoint_instance_to_str(struct zmk_endpoint_instance endpoint, char *str, size_t len) {
+    switch (endpoint.transport) {
+    case ZMK_TRANSPORT_USB:
+        return snprintf(str, len, "USB");
+
+    case ZMK_TRANSPORT_BLE:
+        return snprintf(str, len, "BLE:%d", endpoint.ble.profile_index);
+
+    default:
+        return snprintf(str, len, "Invalid");
+    }
+}
+
+#define INSTANCE_INDEX_OFFSET_USB 0
+#define INSTANCE_INDEX_OFFSET_BLE ZMK_ENDPOINT_USB_COUNT
+
+int zmk_endpoint_instance_to_index(struct zmk_endpoint_instance endpoint) {
+    switch (endpoint.transport) {
+    case ZMK_TRANSPORT_USB:
+        return INSTANCE_INDEX_OFFSET_USB;
+
+    case ZMK_TRANSPORT_BLE:
+        return INSTANCE_INDEX_OFFSET_BLE + endpoint.ble.profile_index;
+    }
+
+    LOG_ERR("Invalid transport %d", endpoint.transport);
+    return 0;
+}
+
+int zmk_endpoints_select_transport(enum zmk_transport transport) {
+    LOG_DBG("Selected endpoint transport %d", transport);
+
+    if (preferred_transport == transport) {
         return 0;
     }
 
-    preferred_endpoint = endpoint;
+    preferred_transport = transport;
 
     endpoints_save_preferred();
 
@@ -62,20 +108,22 @@ int zmk_endpoints_select(enum zmk_endpoint endpoint) {
     return 0;
 }
 
-enum zmk_endpoint zmk_endpoints_selected() { return current_endpoint; }
-
-int zmk_endpoints_toggle() {
-    enum zmk_endpoint new_endpoint =
-        (preferred_endpoint == ZMK_ENDPOINT_USB) ? ZMK_ENDPOINT_BLE : ZMK_ENDPOINT_USB;
-    return zmk_endpoints_select(new_endpoint);
+int zmk_endpoints_toggle_transport(void) {
+    enum zmk_transport new_transport =
+        (preferred_transport == ZMK_TRANSPORT_USB) ? ZMK_TRANSPORT_BLE : ZMK_TRANSPORT_USB;
+    return zmk_endpoints_select_transport(new_transport);
 }
 
-static int send_keyboard_report() {
+struct zmk_endpoint_instance zmk_endpoints_selected(void) {
+    return current_instance;
+}
+
+static int send_keyboard_report(void) {
     struct zmk_hid_keyboard_report *keyboard_report = zmk_hid_get_keyboard_report();
 
-    switch (current_endpoint) {
+    switch (current_instance.transport) {
 #if IS_ENABLED(CONFIG_ZMK_USB)
-    case ZMK_ENDPOINT_USB: {
+    case ZMK_TRANSPORT_USB: {
         int err = zmk_usb_hid_send_report((uint8_t *)keyboard_report, sizeof(*keyboard_report));
         if (err) {
             LOG_ERR("FAILED TO SEND OVER USB: %d", err);
@@ -85,7 +133,7 @@ static int send_keyboard_report() {
 #endif /* IS_ENABLED(CONFIG_ZMK_USB) */
 
 #if IS_ENABLED(CONFIG_ZMK_BLE)
-    case ZMK_ENDPOINT_BLE: {
+    case ZMK_TRANSPORT_BLE: {
         int err = zmk_hog_send_keyboard_report(&keyboard_report->body);
         if (err) {
             LOG_ERR("FAILED TO SEND OVER HOG: %d", err);
@@ -93,19 +141,18 @@ static int send_keyboard_report() {
         return err;
     }
 #endif /* IS_ENABLED(CONFIG_ZMK_BLE) */
-
-    default:
-        LOG_ERR("Unsupported endpoint %d", current_endpoint);
-        return -ENOTSUP;
     }
+
+    LOG_ERR("Unsupported endpoint transport %d", current_instance.transport);
+    return -ENOTSUP;
 }
 
-static int send_consumer_report() {
+static int send_consumer_report(void) {
     struct zmk_hid_consumer_report *consumer_report = zmk_hid_get_consumer_report();
 
-    switch (current_endpoint) {
+    switch (current_instance.transport) {
 #if IS_ENABLED(CONFIG_ZMK_USB)
-    case ZMK_ENDPOINT_USB: {
+    case ZMK_TRANSPORT_USB: {
         int err = zmk_usb_hid_send_report((uint8_t *)consumer_report, sizeof(*consumer_report));
         if (err) {
             LOG_ERR("FAILED TO SEND OVER USB: %d", err);
@@ -115,7 +162,7 @@ static int send_consumer_report() {
 #endif /* IS_ENABLED(CONFIG_ZMK_USB) */
 
 #if IS_ENABLED(CONFIG_ZMK_BLE)
-    case ZMK_ENDPOINT_BLE: {
+    case ZMK_TRANSPORT_BLE: {
         int err = zmk_hog_send_consumer_report(&consumer_report->body);
         if (err) {
             LOG_ERR("FAILED TO SEND OVER HOG: %d", err);
@@ -123,11 +170,10 @@ static int send_consumer_report() {
         return err;
     }
 #endif /* IS_ENABLED(CONFIG_ZMK_BLE) */
-
-    default:
-        LOG_ERR("Unsupported endpoint %d", current_endpoint);
-        return -ENOTSUP;
     }
+
+    LOG_ERR("Unsupported endpoint transport %d", current_instance.transport);
+    return -ENOTSUP;
 }
 
 int zmk_endpoints_send_report(uint16_t usage_page) {
@@ -136,12 +182,13 @@ int zmk_endpoints_send_report(uint16_t usage_page) {
     switch (usage_page) {
     case HID_USAGE_KEY:
         return send_keyboard_report();
+
     case HID_USAGE_CONSUMER:
         return send_consumer_report();
-    default:
-        LOG_ERR("Unsupported usage page %d", usage_page);
-        return -ENOTSUP;
     }
+
+    LOG_ERR("Unsupported usage page %d", usage_page);
+    return -ENOTSUP;
 }
 
 #if IS_ENABLED(CONFIG_SETTINGS)
@@ -151,12 +198,12 @@ static int endpoints_handle_set(const char *name, size_t len, settings_read_cb r
     LOG_DBG("Setting endpoint value %s", name);
 
     if (settings_name_steq(name, "preferred", NULL)) {
-        if (len != sizeof(enum zmk_endpoint)) {
-            LOG_ERR("Invalid endpoint size (got %d expected %d)", len, sizeof(enum zmk_endpoint));
+        if (len != sizeof(enum zmk_transport)) {
+            LOG_ERR("Invalid endpoint size (got %d expected %d)", len, sizeof(enum zmk_transport));
             return -EINVAL;
         }
 
-        int err = read_cb(cb_arg, &preferred_endpoint, sizeof(enum zmk_endpoint));
+        int err = read_cb(cb_arg, &preferred_transport, sizeof(enum zmk_transport));
         if (err <= 0) {
             LOG_ERR("Failed to read preferred endpoint from settings (err %d)", err);
             return err;
@@ -171,6 +218,60 @@ static int endpoints_handle_set(const char *name, size_t len, settings_read_cb r
 struct settings_handler endpoints_handler = {.name = "endpoints", .h_set = endpoints_handle_set};
 #endif /* IS_ENABLED(CONFIG_SETTINGS) */
 
+static bool is_usb_ready(void) {
+#if IS_ENABLED(CONFIG_ZMK_USB)
+    return zmk_usb_is_hid_ready();
+#else
+    return false;
+#endif
+}
+
+static bool is_ble_ready(void) {
+#if IS_ENABLED(CONFIG_ZMK_BLE)
+    return zmk_ble_active_profile_is_connected();
+#else
+    return false;
+#endif
+}
+
+static enum zmk_transport get_selected_transport(void) {
+    if (is_ble_ready()) {
+        if (is_usb_ready()) {
+            LOG_DBG("Both endpoint transports are ready. Using %d", preferred_transport);
+            return preferred_transport;
+        }
+
+        LOG_DBG("Only BLE is ready.");
+        return ZMK_TRANSPORT_BLE;
+    }
+
+    if (is_usb_ready()) {
+        LOG_DBG("Only USB is ready.");
+        return ZMK_TRANSPORT_USB;
+    }
+
+    LOG_DBG("No endpoint transports are ready.");
+    return DEFAULT_TRANSPORT;
+}
+
+static struct zmk_endpoint_instance get_selected_instance(void) {
+    struct zmk_endpoint_instance instance = {.transport = get_selected_transport()};
+
+    switch (instance.transport) {
+#if IS_ENABLED(CONFIG_ZMK_BLE)
+    case ZMK_TRANSPORT_BLE:
+        instance.ble.profile_index = zmk_ble_active_profile_index();
+        break;
+#endif // IS_ENABLED(CONFIG_ZMK_BLE)
+
+    default:
+        // No extra data for this transport.
+        break;
+    }
+
+    return instance;
+}
+
 static int zmk_endpoints_init(const struct device *_arg) {
 #if IS_ENABLED(CONFIG_SETTINGS)
     settings_subsys_init();
@@ -186,45 +287,11 @@ static int zmk_endpoints_init(const struct device *_arg) {
     settings_load_subtree("endpoints");
 #endif
 
+    current_instance = get_selected_instance();
+
     return 0;
 }
 
-static bool is_usb_ready() {
-#if IS_ENABLED(CONFIG_ZMK_USB)
-    return zmk_usb_is_hid_ready();
-#else
-    return false;
-#endif
-}
-
-static bool is_ble_ready() {
-#if IS_ENABLED(CONFIG_ZMK_BLE)
-    return zmk_ble_active_profile_is_connected();
-#else
-    return false;
-#endif
-}
-
-static enum zmk_endpoint get_selected_endpoint() {
-    if (is_ble_ready()) {
-        if (is_usb_ready()) {
-            LOG_DBG("Both endpoints are ready. Using %d", preferred_endpoint);
-            return preferred_endpoint;
-        }
-
-        LOG_DBG("Only BLE is ready.");
-        return ZMK_ENDPOINT_BLE;
-    }
-
-    if (is_usb_ready()) {
-        LOG_DBG("Only USB is ready.");
-        return ZMK_ENDPOINT_USB;
-    }
-
-    LOG_DBG("No endpoints are ready.");
-    return DEFAULT_ENDPOINT;
-}
-
 static void disconnect_current_endpoint() {
     zmk_hid_keyboard_clear();
     zmk_hid_consumer_clear();
@@ -233,18 +300,21 @@ static void disconnect_current_endpoint() {
     zmk_endpoints_send_report(HID_USAGE_CONSUMER);
 }
 
-static void update_current_endpoint() {
-    enum zmk_endpoint new_endpoint = get_selected_endpoint();
+static void update_current_endpoint(void) {
+    struct zmk_endpoint_instance new_instance = get_selected_instance();
 
-    if (new_endpoint != current_endpoint) {
-        /* Cancel all current keypresses so keys don't stay held on the old endpoint. */
+    if (!zmk_endpoint_instance_eq(new_instance, current_instance)) {
+        // Cancel all current keypresses so keys don't stay held on the old endpoint.
         disconnect_current_endpoint();
 
-        current_endpoint = new_endpoint;
-        LOG_INF("Endpoint changed: %d", current_endpoint);
+        current_instance = new_instance;
 
-        ZMK_EVENT_RAISE(new_zmk_endpoint_selection_changed(
-            (struct zmk_endpoint_selection_changed){.endpoint = current_endpoint}));
+        char endpoint_str[ZMK_ENDPOINT_STR_LEN];
+        zmk_endpoint_instance_to_str(current_instance, endpoint_str, sizeof(endpoint_str));
+        LOG_INF("Endpoint changed: %s", endpoint_str);
+
+        ZMK_EVENT_RAISE(
+            new_zmk_endpoint_changed((struct zmk_endpoint_changed){.endpoint = current_instance}));
     }
 }
 
diff --git a/app/src/events/endpoint_selection_changed.c b/app/src/events/endpoint_selection_changed.c
index 34bc39dd..6b152156 100644
--- a/app/src/events/endpoint_selection_changed.c
+++ b/app/src/events/endpoint_selection_changed.c
@@ -5,6 +5,6 @@
  */
 
 #include <zephyr/kernel.h>
-#include <zmk/events/endpoint_selection_changed.h>
+#include <zmk/events/endpoint_changed.h>
 
-ZMK_EVENT_IMPL(zmk_endpoint_selection_changed);
+ZMK_EVENT_IMPL(zmk_endpoint_changed);