diff --git a/epicardium/ble/ble_main.c b/epicardium/ble/ble_main.c
index ec7d090de6f3fc817dd284e3a87083c78b0b4b4c..ce33c5bc2e66c6b0fd73157970ddb42f69b58b22 100644
--- a/epicardium/ble/ble_main.c
+++ b/epicardium/ble/ble_main.c
@@ -39,8 +39,13 @@
 #include "rscp/rscp_api.h"
 #include "cccd.h"
 
+#include "epicardium.h"
+#include "api/interrupt-sender.h"
 #include "modules/log.h"
 
+static bool active;
+static enum ble_event_type ble_event;
+
 /**************************************************************************************************
   Macros
 **************************************************************************************************/
@@ -191,6 +196,8 @@ static const attsCccSet_t bleCccSet[BLE_NUM_CCC_IDX] =
 /*! WSF handler ID */
 wsfHandlerId_t bleHandlerId;
 
+static dmConnId_t pair_connId = DM_CONN_ID_NONE;
+static uint32_t pair_confirm_value;
 
 static void BleHandler(wsfEventMask_t event, wsfMsgHdr_t *pMsg);
 /*************************************************************************************************/
@@ -348,27 +355,87 @@ static void bleSetup(bleMsg_t *pMsg)
   AppAdvSetData(APP_ADV_DATA_CONNECTABLE, 0, NULL);
   AppAdvSetData(APP_SCAN_DATA_CONNECTABLE, 0, NULL);
 
-#if 0
-  /* TODO: card10: until we have an BLE dialog, be discoverable and bondable always */
-  /* start advertising; automatically set connectable/discoverable mode and bondable mode */
-  AppAdvStart(APP_MODE_AUTO_INIT);
-#else
-  /* enter discoverable and bondable mode mode by default */
-  AppSetBondable(TRUE);
-  AppAdvStart(APP_MODE_DISCOVERABLE);
-#endif
+  /* We only want to be bondable when the appropriate dialog is open */
+  AppSetBondable(FALSE);
+  /* TODO: Sadly, not advertising leads to a higher current consumption... */
+  if(AppDbCheckBonded() == FALSE) {
+    AppAdvStop();
+  } else {
+    AppAdvStart(APP_MODE_CONNECTABLE);
+  }
+  active = true;
+}
+
+void epic_ble_set_bondable(bool bondable)
+{
+	if(!active) {
+		return;
+	}
+
+	if(bondable) {
+		LOG_INFO("ble", "Making bondable and discoverable");
+		/* We need to stop advertising in between or the
+		 * adv set will not be changed... */
+		AppAdvStop();
+		AppSetBondable(TRUE);
+		AppAdvStart(APP_MODE_DISCOVERABLE);
+	} else {
+		LOG_INFO("ble", "Making connectable");
+		AppAdvStop();
+		AppSetBondable(FALSE);
+		if(AppDbCheckBonded()) {
+			AppAdvStart(APP_MODE_CONNECTABLE);
+		}
+	}
 }
 
-void bleHandleNumericComparison(dmSecCnfIndEvt_t *pCnfInd)
+uint32_t epic_ble_get_compare_value(void)
 {
-	uint32_t confirm = DmSecGetCompareValue(pCnfInd->confirm);
-	dmConnId_t pair_connId;
-	APP_TRACE_INFO1(">>> Confirm Value: %d <<<", confirm);
-	LOG_INFO("ble", "Confirm Value: %ld", confirm);
+	return pair_confirm_value;
+}
+
+void epic_ble_compare_response(bool confirmed)
+{
+	if(!active) {
+		return;
+	}
+
+	if(pair_connId != DM_CONN_ID_NONE) {
+		LOG_INFO("ble", "Value confirmed: %u", confirmed);
+		DmSecCompareRsp(pair_connId, confirmed);
+	} else {
+		/* error condition */
+	}
+}
+static void trigger_event(enum ble_event_type event)
+{
+	bool enabled;
+	epic_interrupt_is_enabled(EPIC_INT_BLE, &enabled);
+	if(ble_event && enabled) {
+		LOG_WARN("ble", "Application missed event %u", ble_event);
+	}
+
+	ble_event = event;
+	api_interrupt_trigger(EPIC_INT_BLE);
+}
+
+enum ble_event_type epic_ble_get_event(void)
+{
+	enum ble_event_type event = ble_event;
+	ble_event = 0;
+	return event;
+}
+
+static void bleHandleNumericComparison(dmSecCnfIndEvt_t *pCnfInd)
+{
+	if(!active) {
+		return;
+	}
+
 	pair_connId = (dmConnId_t)pCnfInd->hdr.param;
-	/* TODO: Verify that local and peer confirmation values match */
-	LOG_INFO("ble", "Confirming");
-	DmSecCompareRsp(pair_connId, TRUE);
+	pair_confirm_value = DmSecGetCompareValue(pCnfInd->confirm);
+	LOG_INFO("ble", "Confirm Value: %ld", pair_confirm_value);
+	trigger_event(BLE_EVENT_HANDLE_NUMERIC_COMPARISON);
 }
 
 /*************************************************************************************************/
@@ -454,6 +521,8 @@ static void bleProcMsg(bleMsg_t *pMsg)
     case DM_SEC_PAIR_CMPL_IND:
       LOG_INFO("ble", "Secure pairing successful, auth: 0x%02X",
                pMsg->dm.pairCmpl.auth);
+      pair_connId = DM_CONN_ID_NONE;
+      trigger_event(BLE_EVENT_PAIRING_COMPLETE);
       /* After a successful pairing, bonding is disabled again.
        * We don't want that for now. */
       AppSetBondable(TRUE);
@@ -474,6 +543,8 @@ static void bleProcMsg(bleMsg_t *pMsg)
                    pMsg->hdr.status);
           break;
       }
+      pair_connId = DM_CONN_ID_NONE;
+      trigger_event(BLE_EVENT_PAIRING_FAILED);
       break;
 
     case DM_SEC_ENCRYPT_IND:
diff --git a/epicardium/epicardium.h b/epicardium/epicardium.h
index 34a5db597b2e78905cbf5c11f572bb4cc2ae5c9e..3eb57d5e62b46b0e334fc52aa22acb86ff48efd2 100644
--- a/epicardium/epicardium.h
+++ b/epicardium/epicardium.h
@@ -147,6 +147,12 @@ typedef _Bool bool;
 #define API_CONFIG_GET_INTEGER     0x131
 #define API_CONFIG_GET_BOOLEAN     0x132
 #define API_CONFIG_SET_STRING      0x133
+
+#define API_BLE_GET_COMPARE_VALUE  0x140
+#define API_BLE_COMPARE_RESPONSE   0x141
+#define API_BLE_SET_BONDABLE       0x142
+#define API_BLE_GET_EVENT          0x143
+
 /* clang-format on */
 
 typedef uint32_t api_int_id_t;
@@ -223,9 +229,10 @@ API(API_INTERRUPT_IS_ENABLED, int epic_interrupt_is_enabled(api_int_id_t int_id,
 #define EPIC_INT_BHI160_MAGNETOMETER    8
 /** MAX86150 ECG and PPG sensor.  See :c:func:`epic_isr_max86150`. */
 #define EPIC_INT_MAX86150		9
-
+/** Bluetooth Low Energy event.  See :c:func:`epic_isr_ble`. */
+#define EPIC_INT_BLE                    10
 /* Number of defined interrupts. */
-#define EPIC_INT_NUM                    10
+#define EPIC_INT_NUM                    11
 /* clang-format on */
 
 /*
@@ -2095,5 +2102,108 @@ API(API_CONFIG_GET_STRING, int epic_config_get_string(const char *key, char *buf
  * .. versionadded:: 1.16
  */
 API(API_CONFIG_SET_STRING, int epic_config_set_string(const char *key, const char *value));
+
+
+/**
+ * Bluetooth Low Energy (BLE)
+ * ==========================
+ */
+
+/**
+ * BLE event type
+ */
+enum ble_event_type {
+	/** No event pending */
+	BLE_EVENT_NONE                                    = 0,
+	/** Numeric comparison requested */
+	BLE_EVENT_HANDLE_NUMERIC_COMPARISON               = 1,
+	/** A pairing procedure has failed */
+	BLE_EVENT_PAIRING_FAILED                          = 2,
+	/** A pairing procedure has successfully completed */
+	BLE_EVENT_PAIRING_COMPLETE                        = 3,
+};
+
+
+/**
+ * **Interrupt Service Routine** for :c:data:`EPIC_INT_BLE`
+ *
+ * :c:func:`epic_isr_ble` is called when the BLE stack wants to signal an
+ * event to the application. You can use :c:func:`epic_ble_get_event` to obtain
+ * the event which triggered this interrupt.
+ *
+ * Currently supported events:
+ *
+ * :c:data:`BLE_EVENT_HANDLE_NUMERIC_COMPARISON`:
+ *    An ongoing pairing procedure requires a numeric comparison to complete.
+ *    The compare value can be retreived using :c:func:`epic_ble_get_compare_value`.
+ *
+ * :c:data:`BLE_EVENT_PAIRING_FAILED`:
+ *    A pairing procedure failed. The stack automatically went back advertising
+ *    and accepting new pairings.
+ *
+ * :c:data:`BLE_EVENT_PAIRING_COMPLETE`:
+ *    A pairing procedure has completed sucessfully.
+ *    The stack automatically persists the pairing information, creating a bond.
+ *
+ * .. versionadded:: 1.16
+ */
+API_ISR(EPIC_INT_BLE, epic_isr_ble);
+
+/**
+ * Retreive the event which triggered :c:func:`epic_isr_ble`
+ *
+ * The handling code needs to ensure to handle interrupts in a timely
+ * manner as new events will overwrite each other. Reading the event
+ * automatically resets it to :c:data:`BLE_EVENT_NONE`.
+ *
+ * :return: Event which triggered the interrupt.
+ *
+ * .. versionadded:: 1.16
+ */
+API(API_BLE_GET_EVENT, enum ble_event_type epic_ble_get_event(void));
+
+/**
+ * Retrieve the compare value of an ongoing pairing procedure.
+ *
+ * If no pairing procedure is ongoing, the returned value is undefined.
+ *
+ * :return: 6 digit long compare value
+ *
+ * .. versionadded:: 1.16
+ */
+API(API_BLE_GET_COMPARE_VALUE, uint32_t epic_ble_get_compare_value(void));
+
+/**
+ * Indicate wether the user confirmed the compare value.
+ *
+ * If a pariring procedure involving a compare value is ongoing and this
+ * function is called with confirmed set to ``true``, it will try to
+ * proceed and complete the pairing process. If called with ``false``, the
+ * pairing procedure will be aborted.
+ *
+ * :param bool confirmed: `true` if the user confirmed the compare value.
+ *
+ * .. versionadded:: 1.16
+ */
+API(API_BLE_COMPARE_RESPONSE, void epic_ble_compare_response(bool confirmed));
+
+/**
+ * Allow or disallow new bondings to happen
+ *
+ * By default the card10 will not allow new bondings to be made. New
+ * bondings have to explicitly allowed by calling this function.
+ *
+ * While bonadable the card10 will change its advertisements to
+ * indicate to scanning hosts that it is available for discovery.
+ *
+ * When switching applications new bondings are automatically
+ * disallowed.
+ *
+ * :param bool bondable: `true` if new bondings should be allowed.
+ *
+ * .. versionadded:: 1.16
+ */
+API(API_BLE_SET_BONDABLE, void epic_ble_set_bondable(bool bondable));
+
 #endif /* _EPICARDIUM_H */
 
diff --git a/epicardium/modules/hardware.c b/epicardium/modules/hardware.c
index 6d118fe73ef59007c4e3cb915afdba004e678d56..1168f5a756d81ee3e3d16a15e5e4c703314b2b7a 100644
--- a/epicardium/modules/hardware.c
+++ b/epicardium/modules/hardware.c
@@ -289,5 +289,10 @@ int hardware_reset(void)
 
 	epic_max86150_disable_sensor();
 
+	/*
+	 * BLE
+	 */
+	epic_ble_set_bondable(false);
+
 	return 0;
 }
diff --git a/preload/apps/ble/__init__.py b/preload/apps/ble/__init__.py
index acf0b13530997eefaea87483283d978edf938c89..78fa562202bd14135969fd2c984ac7dd543eaacb 100644
--- a/preload/apps/ble/__init__.py
+++ b/preload/apps/ble/__init__.py
@@ -2,17 +2,28 @@ import os
 import display
 import time
 import buttons
+import sys_ble
+import interrupt
 
 CONFIG_NAME = "ble.txt"
 MAC_NAME = "mac.txt"
 ACTIVE_STRING = "active=true"
 INACTIVE_STRING = "active=false"
+ble_event = None
+
+
+def ble_callback(_):
+    global ble_event
+    ble_event = sys_ble.get_event()
 
 
 def init():
     if CONFIG_NAME not in os.listdir("."):
         with open(CONFIG_NAME, "w") as f:
             f.write(INACTIVE_STRING)
+    interrupt.set_callback(interrupt.BLE, ble_callback)
+    interrupt.enable_callback(interrupt.BLE)
+    sys_ble.set_bondable(True)
 
 
 def load_mac():
@@ -67,21 +78,74 @@ def selector():
     disp.print("toggle", posx=25, posy=40, fg=[255, 255, 255])
 
 
-disp = display.open()
-button_pressed = True
 init()
+disp = display.open()
+state = 1
+v_old = buttons.read()
 
 while True:
-    disp.clear()
-    headline()
-    v = buttons.read(buttons.TOP_RIGHT)
-    if v == 0:
-        button_pressed = False
-
-    if not button_pressed and v & buttons.TOP_RIGHT != 0:
-        button_pressed = True
-        toggle()
+    v_new = buttons.read()
+    v = ~v_old & v_new
+    v_old = v_new
+
+    if state == 1:
+        # print config screen
+        disp.clear()
+        headline()
+        selector()
+        disp.update()
+        state = 2
+    elif state == 2:
+        # wait for button press or ble_event
+        if ble_event == sys_ble.EVENT_HANDLE_NUMERIC_COMPARISON:
+            ble_event = None
+            state = 3
+        if v & buttons.TOP_RIGHT:
+            toggle()
+            state = 1
+
+    elif state == 3:
+        # print confirmation value
+        compare_value = sys_ble.get_compare_value()
+        disp.clear()
+        disp.print("confirm:", posy=0, fg=[0, 255, 255])
+        disp.print("%06d" % compare_value, posy=20, fg=[255, 0, 0])
+        disp.update()
+        state = 4
+    elif state == 4:
+        # wait for button press or ble_event
+        if ble_event == sys_ble.EVENT_PAIRING_FAILED:
+            ble_event = None
+            state = 6
+        if v & buttons.BOTTOM_RIGHT:
+            sys_ble.confirm_compare_value(True)
+            disp.clear()
+            disp.print("Wait", posy=0, fg=[0, 255, 255])
+            disp.update()
+            state = 5
+        elif v & buttons.BOTTOM_LEFT:
+            sys_ble.confirm_compare_value(False)
+            state = 1
+
+    elif state == 5:
+        # Wait for pairing to complete
+        if ble_event == sys_ble.EVENT_PAIRING_FAILED:
+            ble_event = None
+            state = 6
+        elif ble_event == sys_ble.EVENT_PAIRING_COMPLETE:
+            ble_event = None
+            disp.clear()
+            disp.print("OK", posy=0, fg=[0, 255, 255])
+            disp.update()
+            time.sleep(5)
+            state = 1
+
+    elif state == 6:
+        # display fail screen and wait 5 seconds
+        disp.clear()
+        disp.print("Fail", posy=0, fg=[0, 255, 255])
+        disp.update()
+        time.sleep(5)
+        state = 1
 
-    selector()
-    disp.update()
     time.sleep(0.1)
diff --git a/pycardium/meson.build b/pycardium/meson.build
index d12e62f5ed6e58cd8699890a404ffc23a4b33f15..bd01afb49f403ec057b533285aef86da173cc334 100644
--- a/pycardium/meson.build
+++ b/pycardium/meson.build
@@ -14,6 +14,7 @@ modsrc = files(
   'modules/os.c',
   'modules/personal_state.c',
   'modules/power.c',
+  'modules/sys_ble.c',
   'modules/sys_bme680.c',
   'modules/sys_display.c',
   'modules/sys_leds.c',
diff --git a/pycardium/modules/interrupt.c b/pycardium/modules/interrupt.c
index ad3b86d73424c91bfe6dde576f92dd4a842da968..1e7fcd89ff85d0940611f37ae170e0321aa04a27 100644
--- a/pycardium/modules/interrupt.c
+++ b/pycardium/modules/interrupt.c
@@ -99,6 +99,7 @@ static const mp_rom_map_elem_t interrupt_module_globals_table[] = {
 	  MP_OBJ_NEW_SMALL_INT(EPIC_INT_MAX30001_ECG) },
 	{ MP_ROM_QSTR(MP_QSTR_MAX86150),
 	  MP_OBJ_NEW_SMALL_INT(EPIC_INT_MAX86150) },
+	{ MP_ROM_QSTR(MP_QSTR_BLE), MP_OBJ_NEW_SMALL_INT(EPIC_INT_BLE) },
 
 };
 static MP_DEFINE_CONST_DICT(
diff --git a/pycardium/modules/qstrdefs.h b/pycardium/modules/qstrdefs.h
index 3a6e9dfcf47dfd16160b9b0cf76809436176bc55..0c15841d0a7e6965bfba2000189cf761b84669d8 100644
--- a/pycardium/modules/qstrdefs.h
+++ b/pycardium/modules/qstrdefs.h
@@ -189,6 +189,20 @@ Q(MAX86150)
 Q(ws2812)
 Q(set_all)
 
+/* config */
 Q(config)
 Q(set_string)
 Q(get_string)
+
+/* BLE */
+Q(BLE)
+Q(ble)
+Q(get_compare_value)
+Q(confirm_compare_value)
+Q(set_bondable)
+Q(get_event)
+Q(EVENT_NONE)
+Q(EVENT_HANDLE_NUMERIC_COMPARISON)
+Q(EVENT_PAIRING_COMPLETE)
+Q(EVENT_PAIRING_FAILED)
+
diff --git a/pycardium/modules/sys_ble.c b/pycardium/modules/sys_ble.c
new file mode 100644
index 0000000000000000000000000000000000000000..d262ce4100889f5c8489384a448b12b14ced653a
--- /dev/null
+++ b/pycardium/modules/sys_ble.c
@@ -0,0 +1,71 @@
+#include "epicardium.h"
+
+#include "py/obj.h"
+#include "py/objlist.h"
+#include "py/runtime.h"
+
+#include <stdint.h>
+
+static mp_obj_t mp_ble_confirm_compare_value(mp_obj_t confirmed_obj)
+{
+	bool confirmed = mp_obj_is_true(confirmed_obj);
+	epic_ble_compare_response(confirmed);
+	return mp_const_none;
+}
+static MP_DEFINE_CONST_FUN_OBJ_1(
+	ble_confirm_compare_value_obj, mp_ble_confirm_compare_value
+);
+
+static mp_obj_t mp_ble_get_compare_value(void)
+{
+	return mp_obj_new_int(epic_ble_get_compare_value());
+}
+static MP_DEFINE_CONST_FUN_OBJ_0(
+	ble_get_compare_value_obj, mp_ble_get_compare_value
+);
+
+static mp_obj_t mp_ble_get_event(void)
+{
+	return mp_obj_new_int(epic_ble_get_event());
+}
+static MP_DEFINE_CONST_FUN_OBJ_0(ble_get_event_obj, mp_ble_get_event);
+
+static mp_obj_t mp_ble_set_bondable(mp_obj_t bondable_obj)
+{
+	bool bondable = mp_obj_is_true(bondable_obj);
+	epic_ble_set_bondable(bondable);
+	return mp_const_none;
+}
+static MP_DEFINE_CONST_FUN_OBJ_1(ble_set_bondable_obj, mp_ble_set_bondable);
+
+static const mp_rom_map_elem_t ble_module_globals_table[] = {
+	{ MP_ROM_QSTR(MP_QSTR___name__), MP_ROM_QSTR(MP_QSTR_sys_ble) },
+	{ MP_ROM_QSTR(MP_QSTR_confirm_compare_value),
+	  MP_ROM_PTR(&ble_confirm_compare_value_obj) },
+	{ MP_ROM_QSTR(MP_QSTR_get_compare_value),
+	  MP_ROM_PTR(&ble_get_compare_value_obj) },
+	{ MP_ROM_QSTR(MP_QSTR_get_event), MP_ROM_PTR(&ble_get_event_obj) },
+	{ MP_ROM_QSTR(MP_QSTR_set_bondable),
+	  MP_ROM_PTR(&ble_set_bondable_obj) },
+
+	/* Event Numbers */
+	{ MP_ROM_QSTR(MP_QSTR_EVENT_NONE),
+	  MP_OBJ_NEW_SMALL_INT(BLE_EVENT_NONE) },
+	{ MP_ROM_QSTR(MP_QSTR_EVENT_HANDLE_NUMERIC_COMPARISON),
+	  MP_OBJ_NEW_SMALL_INT(BLE_EVENT_HANDLE_NUMERIC_COMPARISON) },
+	{ MP_ROM_QSTR(MP_QSTR_EVENT_PAIRING_FAILED),
+	  MP_OBJ_NEW_SMALL_INT(BLE_EVENT_PAIRING_FAILED) },
+	{ MP_ROM_QSTR(MP_QSTR_EVENT_PAIRING_COMPLETE),
+	  MP_OBJ_NEW_SMALL_INT(BLE_EVENT_PAIRING_COMPLETE) },
+
+};
+static MP_DEFINE_CONST_DICT(ble_module_globals, ble_module_globals_table);
+
+const mp_obj_module_t ble_module = {
+	.base    = { &mp_type_module },
+	.globals = (mp_obj_dict_t *)&ble_module_globals,
+};
+
+/* Register the module to make it available in Python */
+/* clang-format off */
+MP_REGISTER_MODULE(MP_QSTR_sys_ble, ble_module, MODULE_BLE_ENABLED);
diff --git a/pycardium/mpconfigport.h b/pycardium/mpconfigport.h
index 463891e0b3fb7db8fcdf33a4255a1218b65e2f82..7a57e34f645b0695cb1a0bb7d712caca65ea89b8 100644
--- a/pycardium/mpconfigport.h
+++ b/pycardium/mpconfigport.h
@@ -71,6 +71,7 @@ int mp_hal_trng_read_int(void);
 #define MODULE_VIBRA_ENABLED                (1)
 #define MODULE_WS2812_ENABLED               (1)
 #define MODULE_CONFIG_ENABLED               (1)
+#define MODULE_BLE_ENABLED                  (1)
 
 /*
  * This port is intended to be 32-bit, but unfortunately, int32_t for