diff --git a/apps/app_confbridge.c b/apps/app_confbridge.c
index 1c6b4648e2c3e25fd05795f740a1d4ecba1091c7..a4e5c67a8d145709cfb68d6bbd388c67a422adca 100644
--- a/apps/app_confbridge.c
+++ b/apps/app_confbridge.c
@@ -576,6 +576,10 @@ static void send_conf_stasis(struct confbridge_conference *conference, struct as
 		return;
 	}
 
+	if (ast_test_flag(&conference->b_profile, BRIDGE_OPT_ENABLE_EVENTS)) {
+		conf_send_event_to_participants(conference, chan, msg);
+	}
+
 	if (channel_topic) {
 		stasis_publish(ast_channel_topic(chan), msg);
 	} else {
@@ -2311,6 +2315,25 @@ static int join_callback(struct ast_bridge_channel *bridge_channel, void *ignore
 	return 0;
 }
 
+struct confbridge_hook_data {
+	struct confbridge_conference *conference;
+	struct confbridge_user *user;
+	enum ast_bridge_hook_type hook_type;
+};
+
+static int send_event_hook_callback(struct ast_bridge_channel *bridge_channel, void *data)
+{
+	struct confbridge_hook_data *hook_data = data;
+
+	if (hook_data->hook_type == AST_BRIDGE_HOOK_TYPE_JOIN) {
+		send_join_event(hook_data->user, hook_data->conference);
+	} else {
+		send_leave_event(hook_data->user, hook_data->conference);
+	}
+
+	return 0;
+}
+
 /*! \brief The ConfBridge application */
 static int confbridge_exec(struct ast_channel *chan, const char *data)
 {
@@ -2328,6 +2351,9 @@ static int confbridge_exec(struct ast_channel *chan, const char *data)
 		.tech_args.silence_threshold = DEFAULT_SILENCE_THRESHOLD,
 		.tech_args.drop_silence = 0,
 	};
+	struct confbridge_hook_data *join_hook_data;
+	struct confbridge_hook_data *leave_hook_data;
+
 	AST_DECLARE_APP_ARGS(args,
 		AST_APP_ARG(conf_name);
 		AST_APP_ARG(b_profile_name);
@@ -2510,8 +2536,39 @@ static int confbridge_exec(struct ast_channel *chan, const char *data)
 
 	conf_moh_unsuspend(&user);
 
-	/* Join our conference bridge for real */
-	send_join_event(&user, conference);
+	join_hook_data = ast_malloc(sizeof(*join_hook_data));
+	if (!join_hook_data) {
+		res = -1;
+		goto confbridge_cleanup;
+	}
+	join_hook_data->user = &user;
+	join_hook_data->conference = conference;
+	join_hook_data->hook_type = AST_BRIDGE_HOOK_TYPE_JOIN;
+	res = ast_bridge_join_hook(&user.features, send_event_hook_callback,
+		join_hook_data, ast_free_ptr, 0);
+	if (res) {
+		ast_free(join_hook_data);
+		ast_log(LOG_ERROR, "Couldn't add bridge join hook for channel '%s'\n", ast_channel_name(chan));
+		goto confbridge_cleanup;
+	}
+
+	leave_hook_data = ast_malloc(sizeof(*leave_hook_data));
+	if (!leave_hook_data) {
+		/* join_hook_data is cleaned up by ast_bridge_features_cleanup via the goto */
+		res = -1;
+		goto confbridge_cleanup;
+	}
+	leave_hook_data->user = &user;
+	leave_hook_data->conference = conference;
+	leave_hook_data->hook_type = AST_BRIDGE_HOOK_TYPE_LEAVE;
+	res = ast_bridge_leave_hook(&user.features, send_event_hook_callback,
+		leave_hook_data, ast_free_ptr, 0);
+	if (res) {
+		/* join_hook_data is cleaned up by ast_bridge_features_cleanup via the goto */
+		ast_free(leave_hook_data);
+		ast_log(LOG_ERROR, "Couldn't add bridge leave hook for channel '%s'\n", ast_channel_name(chan));
+		goto confbridge_cleanup;
+	}
 
 	if (ast_bridge_join_hook(&user.features, join_callback, NULL, NULL, 0)) {
 		async_play_sound_ready(user.chan);
@@ -2533,8 +2590,6 @@ static int confbridge_exec(struct ast_channel *chan, const char *data)
 		pbx_builtin_setvar_helper(chan, "CONFBRIDGE_RESULT", "HANGUP");
 	}
 
-	send_leave_event(&user, conference);
-
 	/* if we're shutting down, don't attempt to do further processing */
 	if (ast_shutting_down()) {
 		/*
diff --git a/apps/confbridge/confbridge_manager.c b/apps/confbridge/confbridge_manager.c
index 51112ba7854bde2895d7ed5c18c8d540389e1180..a7f2fce0146cee88e399c6f18a85b5f584325b74 100644
--- a/apps/confbridge/confbridge_manager.c
+++ b/apps/confbridge/confbridge_manager.c
@@ -395,6 +395,9 @@ static void set_media_labels(struct confbridge_conference *conference,
 	struct ast_stream *stream;
 	struct ast_channel *chan = dir == LABEL_DIRECTION_SRC ? dest_chan : src_chan;
 
+	if (!chan) {
+		return;
+	}
 	topology = ast_channel_get_stream_topology(chan);
 	stream = get_stream(topology, AST_MEDIA_TYPE_VIDEO);
 	if (stream) {
@@ -458,8 +461,8 @@ static void send_message(const char *msg_name, char *conf_name, struct ast_json
 	ast_json_free(json);
 }
 
-static void send_event_to_participants(struct confbridge_conference *conference,
-	struct ast_channel *chan, struct stasis_message * msg)
+void conf_send_event_to_participants(struct confbridge_conference *conference,
+	struct ast_channel *chan, struct stasis_message *msg)
 {
 	struct ast_bridge_blob *obj = stasis_message_data(msg);
 	struct ast_json *extras = obj->blob;
@@ -597,13 +600,6 @@ static void confbridge_publish_manager_event(
 		struct confbridge_conference *conference = conf_find_bridge(conference_name);
 
 		channel_text = ast_manager_build_channel_state_string(blob->channel);
-
-		if (conference && ast_test_flag(&conference->b_profile, BRIDGE_OPT_ENABLE_EVENTS)) {
-			struct ast_channel *chan = ast_channel_get_by_name(blob->channel->name);
-
-			send_event_to_participants(conference, chan, message);
-			ast_channel_cleanup(chan);
-		}
 		ao2_cleanup(conference);
 	}
 
diff --git a/apps/confbridge/include/confbridge.h b/apps/confbridge/include/confbridge.h
index 51ff9a498d7b132ddafefae0ecbc903ffcaa896c..ac403d890ff1357cefdcb84fdf6c259b4cd76ebb 100644
--- a/apps/confbridge/include/confbridge.h
+++ b/apps/confbridge/include/confbridge.h
@@ -701,6 +701,16 @@ int conf_announce_channel_push(struct ast_channel *ast);
  */
 struct confbridge_conference *conf_find_bridge(const char *conference_name);
 
-
+/*!
+ * \brief Send events to bridge participants.
+ * \since 15.7
+ * \since 16.1
+ *
+ * \param conference The conference bridge
+ * \param chan The channel triggering the action
+ * \param msg The stasis message describing the event
+ */
+void conf_send_event_to_participants(struct confbridge_conference *conference,
+	struct ast_channel *chan, struct stasis_message *msg);
 
 #endif