diff --git a/CHANGES b/CHANGES
index d347507435d03c78b4aacf79fa7692d8d8182f2e..60d8a4dea1fd33da97d042dda638a9f319800d2a 100644
--- a/CHANGES
+++ b/CHANGES
@@ -83,6 +83,20 @@ ARI
    types defined in the "disallowed" list are not sent to the application. Note
    that if a type is specified in both lists "disallowed" takes precedence.
 
+ * A new REST API call has been added: 'move'. It follows the format
+   'channels/{channelId}/move' and can be used to move channels from one application
+   to another without needing to exit back into the dialplan. An application must be
+   specified, but the passing a list of arguments to the new application is optional.
+   An example call would look like this:
+
+   client.channels.move(channelId=chan.id, app='ari-example', appArgs='a,b,c')
+
+   If the channel was inside of a bridge when switching applications, it will
+   remain there. If the application specified cannot be moved to, then the channel
+   will remain in the current application and an event will be triggered named
+   "ApplicationMoveFailed", which will provide the destination application's name
+   and the channel information.
+
 res_pjsip
 ------------------
  * A new configuration parameter "taskprocessor_overload_trigger" has been
diff --git a/include/asterisk/stasis_app.h b/include/asterisk/stasis_app.h
index c40e901e5f39dcc7694b9c138d179e5182c7b60a..01c7ff4d7cbd9bff31504cc794de9dbd9473dff6 100644
--- a/include/asterisk/stasis_app.h
+++ b/include/asterisk/stasis_app.h
@@ -501,6 +501,20 @@ void stasis_app_control_clear_roles(struct stasis_app_control *control);
  */
 int stasis_app_control_continue(struct stasis_app_control *control, const char *context, const char *extension, int priority);
 
+/*!
+ * \brief Exit \c res_stasis and move to another Stasis application.
+ *
+ * If the channel is no longer in \c res_stasis, this function does nothing.
+ *
+ * \param control Control for \c res_stasis
+ * \param app_name The name of the application to switch to
+ * \param app_args The list of arguments to pass to the application
+ *
+ * \return 0 for success
+ * \return -1 for error
+ */
+int stasis_app_control_move(struct stasis_app_control *control, const char *app_name, const char *app_args);
+
 /*!
  * \brief Redirect a channel in \c res_stasis to a particular endpoint
  *
diff --git a/res/ari/ari_model_validators.c b/res/ari/ari_model_validators.c
index 44d9d7727cc7daf4ff0d0c0c37a82adc4474999e..254de3c271bf028cc42ba5c5401cd806b0fdb05f 100644
--- a/res/ari/ari_model_validators.c
+++ b/res/ari/ari_model_validators.c
@@ -2028,6 +2028,127 @@ ari_validator ast_ari_validate_mailbox_fn(void)
 	return ast_ari_validate_mailbox;
 }
 
+int ast_ari_validate_application_move_failed(struct ast_json *json)
+{
+	int res = 1;
+	struct ast_json_iter *iter;
+	int has_type = 0;
+	int has_application = 0;
+	int has_args = 0;
+	int has_channel = 0;
+	int has_destination = 0;
+
+	for (iter = ast_json_object_iter(json); iter; iter = ast_json_object_iter_next(json, iter)) {
+		if (strcmp("asterisk_id", ast_json_object_iter_key(iter)) == 0) {
+			int prop_is_valid;
+			prop_is_valid = ast_ari_validate_string(
+				ast_json_object_iter_value(iter));
+			if (!prop_is_valid) {
+				ast_log(LOG_ERROR, "ARI ApplicationMoveFailed field asterisk_id failed validation\n");
+				res = 0;
+			}
+		} else
+		if (strcmp("type", ast_json_object_iter_key(iter)) == 0) {
+			int prop_is_valid;
+			has_type = 1;
+			prop_is_valid = ast_ari_validate_string(
+				ast_json_object_iter_value(iter));
+			if (!prop_is_valid) {
+				ast_log(LOG_ERROR, "ARI ApplicationMoveFailed field type failed validation\n");
+				res = 0;
+			}
+		} else
+		if (strcmp("application", ast_json_object_iter_key(iter)) == 0) {
+			int prop_is_valid;
+			has_application = 1;
+			prop_is_valid = ast_ari_validate_string(
+				ast_json_object_iter_value(iter));
+			if (!prop_is_valid) {
+				ast_log(LOG_ERROR, "ARI ApplicationMoveFailed field application failed validation\n");
+				res = 0;
+			}
+		} else
+		if (strcmp("timestamp", ast_json_object_iter_key(iter)) == 0) {
+			int prop_is_valid;
+			prop_is_valid = ast_ari_validate_date(
+				ast_json_object_iter_value(iter));
+			if (!prop_is_valid) {
+				ast_log(LOG_ERROR, "ARI ApplicationMoveFailed field timestamp failed validation\n");
+				res = 0;
+			}
+		} else
+		if (strcmp("args", ast_json_object_iter_key(iter)) == 0) {
+			int prop_is_valid;
+			has_args = 1;
+			prop_is_valid = ast_ari_validate_list(
+				ast_json_object_iter_value(iter),
+				ast_ari_validate_string);
+			if (!prop_is_valid) {
+				ast_log(LOG_ERROR, "ARI ApplicationMoveFailed field args failed validation\n");
+				res = 0;
+			}
+		} else
+		if (strcmp("channel", ast_json_object_iter_key(iter)) == 0) {
+			int prop_is_valid;
+			has_channel = 1;
+			prop_is_valid = ast_ari_validate_channel(
+				ast_json_object_iter_value(iter));
+			if (!prop_is_valid) {
+				ast_log(LOG_ERROR, "ARI ApplicationMoveFailed field channel failed validation\n");
+				res = 0;
+			}
+		} else
+		if (strcmp("destination", ast_json_object_iter_key(iter)) == 0) {
+			int prop_is_valid;
+			has_destination = 1;
+			prop_is_valid = ast_ari_validate_string(
+				ast_json_object_iter_value(iter));
+			if (!prop_is_valid) {
+				ast_log(LOG_ERROR, "ARI ApplicationMoveFailed field destination failed validation\n");
+				res = 0;
+			}
+		} else
+		{
+			ast_log(LOG_ERROR,
+				"ARI ApplicationMoveFailed has undocumented field %s\n",
+				ast_json_object_iter_key(iter));
+			res = 0;
+		}
+	}
+
+	if (!has_type) {
+		ast_log(LOG_ERROR, "ARI ApplicationMoveFailed missing required field type\n");
+		res = 0;
+	}
+
+	if (!has_application) {
+		ast_log(LOG_ERROR, "ARI ApplicationMoveFailed missing required field application\n");
+		res = 0;
+	}
+
+	if (!has_args) {
+		ast_log(LOG_ERROR, "ARI ApplicationMoveFailed missing required field args\n");
+		res = 0;
+	}
+
+	if (!has_channel) {
+		ast_log(LOG_ERROR, "ARI ApplicationMoveFailed missing required field channel\n");
+		res = 0;
+	}
+
+	if (!has_destination) {
+		ast_log(LOG_ERROR, "ARI ApplicationMoveFailed missing required field destination\n");
+		res = 0;
+	}
+
+	return res;
+}
+
+ari_validator ast_ari_validate_application_move_failed_fn(void)
+{
+	return ast_ari_validate_application_move_failed;
+}
+
 int ast_ari_validate_application_replaced(struct ast_json *json)
 {
 	int res = 1;
@@ -5095,6 +5216,9 @@ int ast_ari_validate_event(struct ast_json *json)
 	if (strcmp("Event", discriminator) == 0) {
 		/* Self type; fall through */
 	} else
+	if (strcmp("ApplicationMoveFailed", discriminator) == 0) {
+		return ast_ari_validate_application_move_failed(json);
+	} else
 	if (strcmp("ApplicationReplaced", discriminator) == 0) {
 		return ast_ari_validate_application_replaced(json);
 	} else
@@ -5293,6 +5417,9 @@ int ast_ari_validate_message(struct ast_json *json)
 	if (strcmp("Message", discriminator) == 0) {
 		/* Self type; fall through */
 	} else
+	if (strcmp("ApplicationMoveFailed", discriminator) == 0) {
+		return ast_ari_validate_application_move_failed(json);
+	} else
 	if (strcmp("ApplicationReplaced", discriminator) == 0) {
 		return ast_ari_validate_application_replaced(json);
 	} else
diff --git a/res/ari/ari_model_validators.h b/res/ari/ari_model_validators.h
index 1ee74f4865b83eaaf54b1a74b333dfb40f2040f5..4b3269af6a73d3fd9f3492e45c3e7dde97562f2f 100644
--- a/res/ari/ari_model_validators.h
+++ b/res/ari/ari_model_validators.h
@@ -623,6 +623,24 @@ int ast_ari_validate_mailbox(struct ast_json *json);
  */
 ari_validator ast_ari_validate_mailbox_fn(void);
 
+/*!
+ * \brief Validator for ApplicationMoveFailed.
+ *
+ * Notification that trying to move a channel to another Stasis application failed.
+ *
+ * \param json JSON object to validate.
+ * \returns True (non-zero) if valid.
+ * \returns False (zero) if invalid.
+ */
+int ast_ari_validate_application_move_failed(struct ast_json *json);
+
+/*!
+ * \brief Function pointer to ast_ari_validate_application_move_failed().
+ *
+ * See \ref ast_ari_model_validators.h for more details.
+ */
+ari_validator ast_ari_validate_application_move_failed_fn(void);
+
 /*!
  * \brief Validator for ApplicationReplaced.
  *
@@ -1527,6 +1545,14 @@ ari_validator ast_ari_validate_application_fn(void);
  * - name: string (required)
  * - new_messages: int (required)
  * - old_messages: int (required)
+ * ApplicationMoveFailed
+ * - asterisk_id: string
+ * - type: string (required)
+ * - application: string (required)
+ * - timestamp: Date
+ * - args: List[string] (required)
+ * - channel: Channel (required)
+ * - destination: string (required)
  * ApplicationReplaced
  * - asterisk_id: string
  * - type: string (required)
diff --git a/res/ari/resource_channels.c b/res/ari/resource_channels.c
index 08f97f1d34b68e3f5810bc555f2a3c5c144c092c..eca70cec8a7a972a7da44b0fe6f7262c805dc938 100644
--- a/res/ari/resource_channels.c
+++ b/res/ari/resource_channels.c
@@ -217,6 +217,26 @@ void ast_ari_channels_continue_in_dialplan(
 	ast_ari_response_no_content(response);
 }
 
+void ast_ari_channels_move(struct ast_variable *headers,
+	struct ast_ari_channels_move_args *args,
+	struct ast_ari_response *response)
+{
+	RAII_VAR(struct stasis_app_control *, control, NULL, ao2_cleanup);
+
+	control = find_control(response, args->channel_id);
+	if (!control) {
+		return;
+	}
+
+	if (stasis_app_control_move(control, args->app, args->app_args)) {
+		ast_ari_response_error(response, 500, "Internal Server Error",
+			"Failed to switch Stasis applications");
+		return;
+	}
+
+	ast_ari_response_no_content(response);
+}
+
 void ast_ari_channels_redirect(struct ast_variable *headers,
 	struct ast_ari_channels_redirect_args *args,
 	struct ast_ari_response *response)
diff --git a/res/ari/resource_channels.h b/res/ari/resource_channels.h
index b071d08baf237a59bcbbc7b2994e16783770cdeb..fdd7a6b7812d775bab15afa202a180567a35561a 100644
--- a/res/ari/resource_channels.h
+++ b/res/ari/resource_channels.h
@@ -261,6 +261,34 @@ int ast_ari_channels_continue_in_dialplan_parse_body(
  * \param[out] response HTTP response
  */
 void ast_ari_channels_continue_in_dialplan(struct ast_variable *headers, struct ast_ari_channels_continue_in_dialplan_args *args, struct ast_ari_response *response);
+/*! Argument struct for ast_ari_channels_move() */
+struct ast_ari_channels_move_args {
+	/*! Channel's id */
+	const char *channel_id;
+	/*! The channel will be passed to this Stasis application. */
+	const char *app;
+	/*! The application arguments to pass to the Stasis application provided by 'app'. */
+	const char *app_args;
+};
+/*!
+ * \brief Body parsing function for /channels/{channelId}/move.
+ * \param body The JSON body from which to parse parameters.
+ * \param[out] args The args structure to parse into.
+ * \retval zero on success
+ * \retval non-zero on failure
+ */
+int ast_ari_channels_move_parse_body(
+	struct ast_json *body,
+	struct ast_ari_channels_move_args *args);
+
+/*!
+ * \brief Move the channel from one Stasis application to another.
+ *
+ * \param headers HTTP headers
+ * \param args Swagger parameters
+ * \param[out] response HTTP response
+ */
+void ast_ari_channels_move(struct ast_variable *headers, struct ast_ari_channels_move_args *args, struct ast_ari_response *response);
 /*! Argument struct for ast_ari_channels_redirect() */
 struct ast_ari_channels_redirect_args {
 	/*! Channel's id */
diff --git a/res/res_ari_channels.c b/res/res_ari_channels.c
index dae146c43c0a93a1345d04f635960596fb6ea62e..3d96d60e4cd8d74f17fc3460222bd1f763f7481b 100644
--- a/res/res_ari_channels.c
+++ b/res/res_ari_channels.c
@@ -775,6 +775,95 @@ static void ast_ari_channels_continue_in_dialplan_cb(
 	}
 #endif /* AST_DEVMODE */
 
+fin: __attribute__((unused))
+	return;
+}
+int ast_ari_channels_move_parse_body(
+	struct ast_json *body,
+	struct ast_ari_channels_move_args *args)
+{
+	struct ast_json *field;
+	/* Parse query parameters out of it */
+	field = ast_json_object_get(body, "app");
+	if (field) {
+		args->app = ast_json_string_get(field);
+	}
+	field = ast_json_object_get(body, "appArgs");
+	if (field) {
+		args->app_args = ast_json_string_get(field);
+	}
+	return 0;
+}
+
+/*!
+ * \brief Parameter parsing callback for /channels/{channelId}/move.
+ * \param get_params GET parameters in the HTTP request.
+ * \param path_vars Path variables extracted from the request.
+ * \param headers HTTP headers.
+ * \param[out] response Response to the HTTP request.
+ */
+static void ast_ari_channels_move_cb(
+	struct ast_tcptls_session_instance *ser,
+	struct ast_variable *get_params, struct ast_variable *path_vars,
+	struct ast_variable *headers, struct ast_json *body, struct ast_ari_response *response)
+{
+	struct ast_ari_channels_move_args args = {};
+	struct ast_variable *i;
+#if defined(AST_DEVMODE)
+	int is_valid;
+	int code;
+#endif /* AST_DEVMODE */
+
+	for (i = get_params; i; i = i->next) {
+		if (strcmp(i->name, "app") == 0) {
+			args.app = (i->value);
+		} else
+		if (strcmp(i->name, "appArgs") == 0) {
+			args.app_args = (i->value);
+		} else
+		{}
+	}
+	for (i = path_vars; i; i = i->next) {
+		if (strcmp(i->name, "channelId") == 0) {
+			args.channel_id = (i->value);
+		} else
+		{}
+	}
+	if (ast_ari_channels_move_parse_body(body, &args)) {
+		ast_ari_response_alloc_failed(response);
+		goto fin;
+	}
+	ast_ari_channels_move(headers, &args, response);
+#if defined(AST_DEVMODE)
+	code = response->response_code;
+
+	switch (code) {
+	case 0: /* Implementation is still a stub, or the code wasn't set */
+		is_valid = response->message == NULL;
+		break;
+	case 500: /* Internal Server Error */
+	case 501: /* Not Implemented */
+	case 404: /* Channel not found */
+	case 409: /* Channel not in a Stasis application */
+		is_valid = 1;
+		break;
+	default:
+		if (200 <= code && code <= 299) {
+			is_valid = ast_ari_validate_void(
+				response->message);
+		} else {
+			ast_log(LOG_ERROR, "Invalid error response %d for /channels/{channelId}/move\n", code);
+			is_valid = 0;
+		}
+	}
+
+	if (!is_valid) {
+		ast_log(LOG_ERROR, "Response validation failed for /channels/{channelId}/move\n");
+		ast_ari_response_error(response, 500,
+			"Internal Server Error", "Response validation failed");
+	}
+#endif /* AST_DEVMODE */
+
 fin: __attribute__((unused))
 	return;
 }
@@ -2680,6 +2769,15 @@ static struct stasis_rest_handlers channels_channelId_continue = {
 	.children = {  }
 };
 /*! \brief REST handler for /api-docs/channels.json */
+static struct stasis_rest_handlers channels_channelId_move = {
+	.path_segment = "move",
+	.callbacks = {
+		[AST_HTTP_POST] = ast_ari_channels_move_cb,
+	},
+	.num_children = 0,
+	.children = {  }
+};
+/*! \brief REST handler for /api-docs/channels.json */
 static struct stasis_rest_handlers channels_channelId_redirect = {
 	.path_segment = "redirect",
 	.callbacks = {
@@ -2831,8 +2929,8 @@ static struct stasis_rest_handlers channels_channelId = {
 		[AST_HTTP_POST] = ast_ari_channels_originate_with_id_cb,
 		[AST_HTTP_DELETE] = ast_ari_channels_hangup_cb,
 	},
-	.num_children = 14,
-	.children = { &channels_channelId_continue,&channels_channelId_redirect,&channels_channelId_answer,&channels_channelId_ring,&channels_channelId_dtmf,&channels_channelId_mute,&channels_channelId_hold,&channels_channelId_moh,&channels_channelId_silence,&channels_channelId_play,&channels_channelId_record,&channels_channelId_variable,&channels_channelId_snoop,&channels_channelId_dial, }
+	.num_children = 15,
+	.children = { &channels_channelId_continue,&channels_channelId_move,&channels_channelId_redirect,&channels_channelId_answer,&channels_channelId_ring,&channels_channelId_dtmf,&channels_channelId_mute,&channels_channelId_hold,&channels_channelId_moh,&channels_channelId_silence,&channels_channelId_play,&channels_channelId_record,&channels_channelId_variable,&channels_channelId_snoop,&channels_channelId_dial, }
 };
 /*! \brief REST handler for /api-docs/channels.json */
 static struct stasis_rest_handlers channels = {
diff --git a/res/res_stasis.c b/res/res_stasis.c
index 4da78a6dc0fe25c19b6e6d243d28126189c27db4..4b7c3ed74cf2f643c18dc9528adaf68e3ee3e17b 100644
--- a/res/res_stasis.c
+++ b/res/res_stasis.c
@@ -1324,7 +1324,17 @@ int stasis_app_exec(struct ast_channel *chan, const char *app_name, int argc,
 
 	control = control_create(chan, app);
 	if (!control) {
-		ast_log(LOG_ERROR, "Allocated failed\n");
+		ast_log(LOG_ERROR, "Control allocation failed or Stasis app '%s' not registered\n", app_name);
+		return -1;
+	}
+
+	if (!control_app(control)) {
+		ast_log(LOG_ERROR, "Stasis app '%s' not registered\n", app_name);
+		return -1;
+	}
+
+	if (!app_is_active(control_app(control))) {
+		ast_log(LOG_ERROR, "Stasis app '%s' not active\n", app_name);
 		return -1;
 	}
 	ao2_link(app_controls, control);
@@ -1334,7 +1344,7 @@ int stasis_app_exec(struct ast_channel *chan, const char *app_name, int argc,
 		return -1;
 	}
 
-	res = send_start_msg(app, chan, argc, argv);
+	res = send_start_msg(control_app(control), chan, argc, argv);
 	if (res != 0) {
 		ast_log(LOG_ERROR,
 			"Error sending start message to '%s'\n", app_name);
@@ -1357,15 +1367,138 @@ int stasis_app_exec(struct ast_channel *chan, const char *app_name, int argc,
 			break;
 		}
 
+		/* control->next_app is only modified within the control thread, so this is safe */
+		if (control_next_app(control)) {
+			struct stasis_app *next_app = ao2_find(apps_registry, control_next_app(control), OBJ_SEARCH_KEY);
+
+			if (next_app && app_is_active(next_app)) {
+				int idx;
+				int next_argc;
+				char **next_argv;
+
+				/* If something goes wrong in this conditional, res will need to be non-zero
+				 * so that the code below the exec loop knows something went wrong during a move.
+				 */
+				if (!stasis_app_channel_is_stasis_end_published(chan)) {
+					res = has_masquerade_store(chan) && app_send_end_msg(control_app(control), chan);
+					if (res != 0) {
+						ast_log(LOG_ERROR,
+							"Error sending end message to %s\n", stasis_app_name(control_app(control)));
+						control_mark_done(control);
+						ao2_ref(next_app, -1);
+						break;
+					}
+				} else {
+					remove_stasis_end_published(chan);
+				}
+
+				/* This will ao2_bump next_app, and unref the previous app by 1 */
+				control_set_app(control, next_app);
+
+				/* There's a chance that the previous application is ready for clean up, so go ahead
+				 * and do that now.
+				 */
+				cleanup();
+
+				/* We need to add another masquerade store, otherwise the leave message will
+				 * not show up for the correct application.
+				 */
+				if (add_masquerade_store(chan)) {
+					ast_log(LOG_ERROR, "Failed to attach masquerade detector\n");
+					res = -1;
+					control_mark_done(control);
+					ao2_ref(next_app, -1);
+					break;
+				}
+
+				/* We MUST get the size before the list, as control_next_app_args steals the elements
+				 * from the string vector.
+				 */
+				next_argc = control_next_app_args_size(control);
+				next_argv = control_next_app_args(control);
+
+				res = send_start_msg(control_app(control), chan, next_argc, next_argv);
+
+				/* Even if res != 0, we still need to free the memory we got from control_argv */
+				if (next_argv) {
+					for (idx = 0; idx < next_argc; idx++) {
+						ast_free(next_argv[idx]);
+					}
+					ast_free(next_argv);
+				}
+
+				if (res != 0) {
+					ast_log(LOG_ERROR,
+						"Error sending start message to '%s'\n", stasis_app_name(control_app(control)));
+					remove_masquerade_store(chan);
+					control_mark_done(control);
+					ao2_ref(next_app, -1);
+					break;
+				}
+
+				/* Done switching applications, free memory and clean up */
+				control_move_cleanup(control);
+			} else {
+				/* If we can't switch applications, do nothing */
+				struct ast_json *msg;
+				RAII_VAR(struct ast_channel_snapshot *, snapshot, NULL, ao2_cleanup);
+
+				if (!next_app) {
+					ast_log(LOG_ERROR, "Could not move to Stasis app '%s' - not registered\n",
+						control_next_app(control));
+				} else {
+					ast_log(LOG_ERROR, "Could not move to Stasis app '%s' - not active\n",
+						control_next_app(control));
+				}
+
+				snapshot = ast_channel_snapshot_get_latest(ast_channel_uniqueid(chan));
+				if (!snapshot) {
+					ast_log(LOG_ERROR, "Could not get channel shapshot for '%s'\n",
+						ast_channel_name(chan));
+				} else {
+					struct ast_json *json_args;
+					int next_argc = control_next_app_args_size(control);
+					char **next_argv = control_next_app_args(control);
+
+					msg = ast_json_pack("{s: s, s: o, s: s, s: []}",
+						"type", "ApplicationMoveFailed",
+						"channel", ast_channel_snapshot_to_json(snapshot, NULL),
+						"destination", control_next_app(control),
+						"args");
+					json_args = ast_json_object_get(msg, "args");
+					if (!json_args) {
+						ast_log(LOG_ERROR, "Could not get args json array");
+					} else {
+						int r = 0;
+						int idx;
+						for (idx = 0; idx < next_argc; ++idx) {
+							r = ast_json_array_append(json_args,
+								ast_json_string_create(next_argv[idx]));
+							if (r != 0) {
+								ast_log(LOG_ERROR, "Error appending to ApplicationMoveFailed message\n");
+								break;
+							}
+						}
+						if (r == 0) {
+							app_send(control_app(control), msg);
+						}
+					}
+					ast_json_unref(msg);
+				}
+			}
+			control_move_cleanup(control);
+			ao2_cleanup(next_app);
+		}
+
 		last_bridge = bridge;
 		bridge = ao2_bump(stasis_app_get_bridge(control));
 
 		if (bridge != last_bridge) {
 			if (last_bridge) {
-				app_unsubscribe_bridge(app, last_bridge);
+				app_unsubscribe_bridge(control_app(control), last_bridge);
 			}
 			if (bridge) {
-				app_subscribe_bridge(app, bridge);
+				app_subscribe_bridge(control_app(control), bridge);
 			}
 		}
 
@@ -1425,18 +1558,18 @@ int stasis_app_exec(struct ast_channel *chan, const char *app_name, int argc,
 	}
 
 	if (stasis_app_get_bridge(control)) {
-		app_unsubscribe_bridge(app, stasis_app_get_bridge(control));
+		app_unsubscribe_bridge(control_app(control), stasis_app_get_bridge(control));
 	}
 	ao2_cleanup(bridge);
 
 	/* Only publish a stasis_end event if it hasn't already been published */
-	if (!stasis_app_channel_is_stasis_end_published(chan)) {
+	if (!res && !stasis_app_channel_is_stasis_end_published(chan)) {
 		/* A masquerade has occurred and this message will be wrong so it
 		 * has already been sent elsewhere. */
-		res = has_masquerade_store(chan) && app_send_end_msg(app, chan);
+		res = has_masquerade_store(chan) && app_send_end_msg(control_app(control), chan);
 		if (res != 0) {
 			ast_log(LOG_ERROR,
-				"Error sending end message to %s\n", app_name);
+				"Error sending end message to %s\n", stasis_app_name(control_app(control)));
 			return res;
 		}
 	} else {
@@ -1456,12 +1589,10 @@ int stasis_app_exec(struct ast_channel *chan, const char *app_name, int argc,
 	/* The control needs to be removed from the controls container in
 	 * case a new PBX is started and ends up coming back into Stasis.
 	 */
-	ao2_cleanup(app);
-	app = NULL;
 	control_unlink(control);
 	control = NULL;
 
-	if (!ast_channel_pbx(chan)) {
+	if (!res && !ast_channel_pbx(chan)) {
 		int chan_hungup;
 
 		/* The ASYNCGOTO softhangup flag may have broken the channel out of
diff --git a/res/stasis/control.c b/res/stasis/control.c
index 5b3b048757c052c218e843109b04069fe9d606dd..3e16e803f23182f768925bba7b507cd904a4fb9b 100644
--- a/res/stasis/control.c
+++ b/res/stasis/control.c
@@ -83,9 +83,19 @@ struct stasis_app_control {
 	 */
 	struct ast_silence_generator *silgen;
 	/*!
-	 * The app for which this control was created
+	 * The app for which this control is currently controlling.
+	 * This can change through the use of the /channels/{channelId}/move
+	 * command.
 	 */
 	struct stasis_app *app;
+	/*!
+	 * The name of the next Stasis application to move to.
+	 */
+	char *next_app;
+	/*!
+	 * The list of arguments to pass to StasisStart when moving to another app.
+	 */
+	AST_VECTOR(, char *) next_app_args;
 	/*!
 	 * When set, /c app_stasis should exit and continue in the dialplan.
 	 */
@@ -101,6 +111,8 @@ static void control_dtor(void *obj)
 	ast_channel_cleanup(control->channel);
 	ao2_cleanup(control->app);
 
+	control_move_cleanup(control);
+
 	ast_cond_destroy(&control->wait_cond);
 	AST_LIST_HEAD_DESTROY(&control->add_rules);
 	AST_LIST_HEAD_DESTROY(&control->remove_rules);
@@ -141,6 +153,9 @@ struct stasis_app_control *control_create(struct ast_channel *channel, struct st
 		return NULL;
 	}
 
+	control->next_app = NULL;
+	AST_VECTOR_INIT(&control->next_app_args, 0);
+
 	return control;
 }
 
@@ -391,6 +406,73 @@ int stasis_app_control_continue(struct stasis_app_control *control, const char *
 	return 0;
 }
 
+struct stasis_app_control_move_data {
+	char *app_name;
+	char *app_args;
+};
+
+static int app_control_move(struct stasis_app_control *control,
+	struct ast_channel *chan, void *data)
+{
+	struct stasis_app_control_move_data *move_data = data;
+
+	control->next_app = ast_strdup(move_data->app_name);
+	if (!control->next_app) {
+		ast_log(LOG_ERROR, "Allocation failed for next app\n");
+		return -1;
+	}
+
+	if (move_data->app_args) {
+		char *token;
+
+		while ((token = strtok_r(move_data->app_args, ",", &move_data->app_args))) {
+			int res;
+			char *arg;
+
+			if (!(arg = ast_strdup(token))) {
+				ast_log(LOG_ERROR, "Allocation failed for next app arg\n");
+				control_move_cleanup(control);
+				return -1;
+			}
+
+			res = AST_VECTOR_APPEND(&control->next_app_args, arg);
+			if (res) {
+				ast_log(LOG_ERROR, "Failed to append arg to next app args\n");
+				ast_free(arg);
+				control_move_cleanup(control);
+				return -1;
+			}
+		}
+	}
+
+	return 0;
+}
+
+int stasis_app_control_move(struct stasis_app_control *control, const char *app_name, const char *app_args)
+{
+	struct stasis_app_control_move_data *move_data;
+	size_t size;
+
+	size = sizeof(*move_data) + strlen(app_name) + strlen(app_args) + 2;
+	if (!(move_data = ast_calloc(1, size))) {
+		return -1;
+	}
+
+	move_data->app_name = (char *)move_data + sizeof(*move_data);
+	move_data->app_args = move_data->app_name + strlen(app_name) + 1;
+
+	strcpy(move_data->app_name, app_name); /* Safe */
+	if (app_args) {
+		strcpy(move_data->app_args, app_args); /* Safe */
+	} else {
+		move_data->app_args = NULL;
+	}
+
+	stasis_app_send_command_async(control, app_control_move, move_data, ast_free_ptr);
+
+	return 0;
+}
+
 static int app_control_redirect(struct stasis_app_control *control,
 	struct ast_channel *chan, void *data)
 {
@@ -1575,3 +1657,32 @@ void stasis_app_control_shutdown(void)
 	}
 	ast_mutex_unlock(&dial_bridge_lock);
 }
+
+void control_set_app(struct stasis_app_control *control, struct stasis_app *app)
+{
+	ao2_cleanup(control->app);
+	control->app = ao2_bump(app);
+}
+
+char *control_next_app(struct stasis_app_control *control)
+{
+	return control->next_app;
+}
+
+void control_move_cleanup(struct stasis_app_control *control)
+{
+	ast_free(control->next_app);
+	control->next_app = NULL;
+
+	AST_VECTOR_RESET(&control->next_app_args, ast_free_ptr);
+}
+
+char **control_next_app_args(struct stasis_app_control *control)
+{
+	return AST_VECTOR_STEAL_ELEMENTS(&control->next_app_args);
+}
+
+int control_next_app_args_size(struct stasis_app_control *control)
+{
+	return AST_VECTOR_SIZE(&control->next_app_args);
+}
diff --git a/res/stasis/control.h b/res/stasis/control.h
index 868a8091bb4014bbcf9067f44e4b8929cdb40849..67aa3b70fe456e07c077db458284f22a995324be 100644
--- a/res/stasis/control.h
+++ b/res/stasis/control.h
@@ -107,6 +107,58 @@ int control_prestart_dispatch_all(struct stasis_app_control *control,
  */
 struct stasis_app *control_app(struct stasis_app_control *control);
 
+/*!
+ * \brief Set the application the control object belongs to
+ *
+ * \param control The control for the channel
+ * \param app The application this control will now belong to
+ *
+ * \note This will unref control's previous app by 1, and bump app by 1
+ */
+void control_set_app(struct stasis_app_control *control, struct stasis_app *app);
+
+/*!
+ * \brief Returns the name of the application we are moving to
+ *
+ * \param control The control for the channel
+ *
+ * \return The name of the application we are moving to
+ */
+char *control_next_app(struct stasis_app_control *control);
+
+/*!
+ * \brief Free any memory that was allocated for switching applications via
+ * /channels/{channelId}/move
+ *
+ * \param control The control for the channel
+ */
+void control_move_cleanup(struct stasis_app_control *control);
+
+/*!
+ * \brief Returns the list of arguments to pass to the application we are moving to
+ *
+ * \note If you wish to get the size of the list, control_next_app_args_size should be
+ * called before this, as this function will steal the elements from the string vector
+ * and set the size to 0.
+ *
+ * \param control The control for the channel
+ *
+ * \return The arguments to pass to the application we are moving to
+ */
+char **control_next_app_args(struct stasis_app_control *control);
+
+/*!
+ * \brief Returns the number of arguments to be passed to the application we are moving to
+ *
+ * \note This should always be called before control_next_app_args, as calling that function
+ * will steal all elements from the string vector and set the size to 0.
+ *
+ * \param control The control for the channel
+ *
+ * \return The number of arguments to be passed to the application we are moving to
+ */
+int control_next_app_args_size(struct stasis_app_control *control);
+
 /*!
  * \brief Command callback for adding a channel to a bridge
  *
diff --git a/rest-api/api-docs/channels.json b/rest-api/api-docs/channels.json
index 08db22467a5615a28abe24f6bcf3f7acc010a510..616193421efba295186241af23bcbfbc2cde9d06 100644
--- a/rest-api/api-docs/channels.json
+++ b/rest-api/api-docs/channels.json
@@ -515,6 +515,54 @@
 				}
 			]
 		},
+		{
+			"path": "/channels/{channelId}/move",
+			"description": "Move the channel from one Stasis application to another.",
+			"operations": [
+				{
+					"httpMethod": "POST",
+					"summary": "Move the channel from one Stasis application to another.",
+					"nickname": "move",
+					"responseClass": "void",
+					"parameters": [
+						{
+							"name": "channelId",
+							"description": "Channel's id",
+							"paramType": "path",
+							"required": true,
+							"allowMultiple": false,
+							"dataType": "string"
+						},
+						{
+							"name": "app",
+							"description": "The channel will be passed to this Stasis application.",
+							"paramType": "query",
+							"required": true,
+							"allowMultiple": false,
+							"dataType": "string"
+						},
+						{
+							"name": "appArgs",
+							"description": "The application arguments to pass to the Stasis application provided by 'app'.",
+							"paramType": "query",
+							"required": false,
+							"allowMultiple": false,
+							"dataType": "string"
+						}
+					],
+					"errorResponses": [
+						{
+							"code": "404",
+							"reason": "Channel not found"
+						},
+						{
+							"code": "409",
+							"reason": "Channel not in a Stasis application"
+						}
+					]
+				}
+			]
+		},
 		{
 			"path": "/channels/{channelId}/redirect",
 			"description": "Inform the channel that it should redirect itself to a different location. Note that this will almost certainly cause the channel to exit the application.",
diff --git a/rest-api/api-docs/events.json b/rest-api/api-docs/events.json
index d85d8d9fe4fabde2b3bcd6cbc36655cb0b2316eb..c9f4b6ae422fb4f2a872f3cb991c074a83e1a283 100644
--- a/rest-api/api-docs/events.json
+++ b/rest-api/api-docs/events.json
@@ -159,6 +159,7 @@
 				"RecordingStarted",
 				"RecordingFinished",
 				"RecordingFailed",
+				"ApplicationMoveFailed",
 				"ApplicationReplaced",
 				"BridgeCreated",
 				"BridgeDestroyed",
@@ -335,6 +336,25 @@
 				}
 			}
 		},
+		"ApplicationMoveFailed": {
+			"id": "ApplicationMoveFailed",
+			"description": "Notification that trying to move a channel to another Stasis application failed.",
+			"properties": {
+				"channel": {
+					"required": true,
+					"type": "Channel"
+				},
+				"destination": {
+					"required": true,
+					"type": "string"
+				},
+				"args": {
+					"required": true,
+					"type": "List[string]",
+					"description": "Arguments to the application"
+				}
+			}
+		},
 		"ApplicationReplaced": {
 			"id": "ApplicationReplaced",
 			"description": "Notification that another WebSocket has taken over for an application.\n\nAn application may only be subscribed to by a single WebSocket at a time. If multiple WebSockets attempt to subscribe to the same application, the newer WebSocket wins, and the older one receives this event.",