diff --git a/include/asterisk/devicestate.h b/include/asterisk/devicestate.h
index a3267387f9925f29e2f7e8c653ef5c534fa37f40..565e013410c3737184445eda6d8e13a5f9bb597b 100644
--- a/include/asterisk/devicestate.h
+++ b/include/asterisk/devicestate.h
@@ -324,6 +324,15 @@ struct stasis_cache *ast_device_state_cache(void);
  */
 struct stasis_message_type *ast_device_state_message_type(void);
 
+/*!
+ * \brief Clear the device from the stasis cache.
+ * \param The device to clear
+ * \retval 0 if successful
+ * \retval -1 nothing to clear
+ * \since 12
+ */
+int ast_device_state_clear_cache(const char *device);
+
 /*!
  * \brief Initialize the device state core
  * \retval 0 Success
diff --git a/include/asterisk/stasis_app.h b/include/asterisk/stasis_app.h
index 4ef55b1937f60c1762d6e4b7dd0fa6fc0e790aea..0c22a6c309764c7adce9b9ab5b6a1191ca1de629 100644
--- a/include/asterisk/stasis_app.h
+++ b/include/asterisk/stasis_app.h
@@ -111,6 +111,18 @@ void stasis_app_unregister(const char *app_name);
  */
 int stasis_app_send(const char *app_name, struct ast_json *message);
 
+/*! \brief Forward declare app */
+struct stasis_app;
+
+/*!
+ * \brief Retrieve an application's name
+ *
+ * \param app An application
+ *
+ * \return The name of the application.
+ */
+const char *stasis_app_name(const struct stasis_app *app);
+
 /*!
  * \brief Return the JSON representation of a Stasis application.
  *
@@ -121,6 +133,102 @@ int stasis_app_send(const char *app_name, struct ast_json *message);
  */
 struct ast_json *stasis_app_to_json(const char *app_name);
 
+/*!
+ * \brief Event source information and callbacks.
+ */
+struct stasis_app_event_source {
+	/*! \brief The scheme to match against on [un]subscribes */
+	const char *scheme;
+
+	/*!
+	 * \brief Find an event source data object by the given id/name.
+	 *
+	 * \param app Application
+	 * \param id A unique identifier to search on
+	 *
+	 * \return The data object associated with the id/name.
+	 */
+	void *(*find)(const struct stasis_app *app, const char *id);
+
+	/*!
+	 * \brief Subscribe an application to an event source.
+	 *
+	 * \param app Application
+	 * \param obj an event source data object
+	 *
+	 * \return 0 on success, failure code otherwise
+	 */
+	int (*subscribe)(struct stasis_app *app, void *obj);
+
+	/*!
+	 * \brief Cancel the subscription an app has to an event source.
+	 *
+	 * \param app Application
+	 * \param id a previously subscribed object id
+	 *
+	 * \return 0 on success, failure code otherwise
+	 */
+	int (*unsubscribe)(struct stasis_app *app, const char *id);
+
+	/*!
+	 * \brief Find an event source by the given id/name.
+	 *
+	 * \param app Application
+	 * \param id A unique identifier to check
+	 *
+	 * \return true if id is subscribed, false otherwise.
+	 */
+	int (*is_subscribed)(struct stasis_app *app, const char *id);
+
+	/*!
+	 * \brief Convert event source data to json
+	 *
+	 * \param app Application
+	 * \param id json object to fill
+	 */
+	void (*to_json)(const struct stasis_app *app, struct ast_json *json);
+
+	/*! Next item in the list */
+	AST_LIST_ENTRY(stasis_app_event_source) next;
+};
+
+/*!
+ * \brief Register an application event source.
+ *
+ * \param obj the event source to register
+ */
+void stasis_app_register_event_source(struct stasis_app_event_source *obj);
+
+/*!
+ * \brief Register core event sources.
+ */
+void stasis_app_register_event_sources(void);
+
+/*!
+ * \brief Checks to see if the given object is a core event source
+ *
+ * \note core event sources are currently only endpoint, bridge, and channel.
+ *
+ * \param obj event source object to check
+ *
+ * \return non-zero if core event source, otherwise 0 (false)
+
+ */
+int stasis_app_is_core_event_source(struct stasis_app_event_source *obj);
+
+/*!
+ * \brief Unregister an application event source.
+ *
+ * \param obj the event source to unregister
+ */
+void stasis_app_unregister_event_source(struct stasis_app_event_source *obj);
+
+/*!
+ * \brief Unregister core event sources.
+ */
+void stasis_app_unregister_event_sources(void);
+
+
 /*! \brief Return code for stasis_app_[un]subscribe */
 enum stasis_app_subscribe_res {
 	STASIS_ASR_OK,
diff --git a/include/asterisk/stasis_app_device_state.h b/include/asterisk/stasis_app_device_state.h
new file mode 100644
index 0000000000000000000000000000000000000000..2bc521a04d2b5fc45a043e5502b46c7f055e6fb4
--- /dev/null
+++ b/include/asterisk/stasis_app_device_state.h
@@ -0,0 +1,95 @@
+/*
+ * Asterisk -- An open source telephony toolkit.
+ *
+ * Copyright (C) 2013, Digium, Inc.
+ *
+ * Kevin Harwell <kharwell@digium.com>
+ *
+ * See http://www.asterisk.org for more information about
+ * the Asterisk project. Please do not directly contact
+ * any of the maintainers of this project for assistance;
+ * the project provides a web site, mailing lists and IRC
+ * channels for your use.
+ *
+ * This program is free software, distributed under the terms of
+ * the GNU General Public License Version 2. See the LICENSE file
+ * at the top of the source tree.
+ */
+
+#ifndef _ASTERISK_STASIS_APP_DEVICE_STATE_H
+#define _ASTERISK_STASIS_APP_DEVICE_STATE_H
+
+/*! \file
+ *
+ * \brief Stasis Application Device State API. See \ref res_stasis "Stasis
+ * Application API" for detailed documentation.
+ *
+ * \author Kevin Harwell <kharwell@digium.com>
+ * \since 12
+ */
+
+#include "asterisk/app.h"
+#include "asterisk/stasis_app.h"
+
+/*! @{ */
+
+/*!
+ * \brief Convert device state to json.
+ *
+ * \param name the name of the device
+ * \param state the device state
+ * \return JSON representation.
+ * \return \c NULL on error.
+ */
+struct ast_json *stasis_app_device_state_to_json(
+	const char *name, enum ast_device_state state);
+
+/*!
+ * \brief Convert device states to json array.
+ *
+ * \return JSON representation.
+ * \return \c NULL on error.
+ */
+struct ast_json *stasis_app_device_states_to_json(void);
+
+/*! Stasis device state application result codes */
+enum stasis_device_state_result {
+	/*! Application controlled device state is okay */
+	STASIS_DEVICE_STATE_OK,
+	/*! The device name is not application controlled */
+	STASIS_DEVICE_STATE_NOT_CONTROLLED,
+	/*! The application controlled device name is missing */
+	STASIS_DEVICE_STATE_MISSING,
+	/*! The application controlled device is unknown */
+	STASIS_DEVICE_STATE_UNKNOWN,
+	/*! The application controlled device has subscribers */
+	STASIS_DEVICE_STATE_SUBSCRIBERS
+};
+
+/*!
+ * \brief Changes the state of a device controlled by ARI.
+ *
+ * \note The controlled device must be prefixed with 'Stasis:'.
+ * \note Implicitly creates the device state.
+ *
+ * \param name the name of the ARI controlled device
+ * \param value a valid device state value
+ *
+ * \return a stasis device state application result.
+ */
+enum stasis_device_state_result stasis_app_device_state_update(
+	const char *name, const char *value);
+
+/*!
+ * \brief Delete a device controlled by ARI.
+ *
+ * \param name the name of the ARI controlled device
+ *
+ * \returna stasis device state application result.
+ */
+enum stasis_device_state_result stasis_app_device_state_delete(
+	const char *name);
+
+/*! @} */
+
+#endif /* _ASTERISK_STASIS_APP_DEVICE_STATE_H */
diff --git a/main/devicestate.c b/main/devicestate.c
index 158d1f817e8302108ddca561efba071a5be9e758..7b52f00af04cfdcd1145d8083ede8426ff43c2ee 100644
--- a/main/devicestate.c
+++ b/main/devicestate.c
@@ -734,6 +734,22 @@ struct stasis_topic *ast_device_state_topic(const char *device)
 	return stasis_topic_pool_get_topic(device_state_topic_pool, device);
 }
 
+int ast_device_state_clear_cache(const char *device)
+{
+	RAII_VAR(struct stasis_message *, cached_msg, NULL, ao2_cleanup);
+	RAII_VAR(struct stasis_message *, msg, NULL, ao2_cleanup);
+
+	if (!(cached_msg = stasis_cache_get(ast_device_state_cache(),
+					    ast_device_state_message_type(), device))) {
+		/* nothing to clear */
+		return -1;
+	}
+
+	msg = stasis_cache_clear_create(cached_msg);
+	stasis_publish(ast_device_state_topic(device), msg);
+	return 0;
+}
+
 int ast_publish_device_state_full(
 			const char *device,
 			enum ast_device_state state,
diff --git a/res/ari.make b/res/ari.make
index 0257f81a9b865a0fa560eb9b96c7ac2ae6d5f7b3..c5dac2d3c070502273ae190f7f9896ca9b7294e4 100644
--- a/res/ari.make
+++ b/res/ari.make
@@ -45,6 +45,10 @@ res_ari_playbacks.so: ari/resource_playbacks.o
 
 ari/resource_playbacks.o: _ASTCFLAGS+=$(call MOD_ASTCFLAGS,res_ari_playbacks)
 
+res_ari_device_states.so: ari/resource_device_states.o
+
+ari/resource_device_states.o: _ASTCFLAGS+=$(call MOD_ASTCFLAGS,res_ari_device_states)
+
 res_ari_events.so: ari/resource_events.o
 
 ari/resource_events.o: _ASTCFLAGS+=$(call MOD_ASTCFLAGS,res_ari_events)
diff --git a/res/ari/ari_model_validators.c b/res/ari/ari_model_validators.c
index 9ea2f94cdf5f93c970a91e00ea4866fa9ce3a402..f84a92ed76c6082e5e5b6bd361b503b7f6eec6f3 100644
--- a/res/ari/ari_model_validators.c
+++ b/res/ari/ari_model_validators.c
@@ -1333,6 +1333,60 @@ ari_validator ast_ari_validate_playback_fn(void)
 	return ast_ari_validate_playback;
 }
 
+int ast_ari_validate_device_state(struct ast_json *json)
+{
+	int res = 1;
+	struct ast_json_iter *iter;
+	int has_name = 0;
+	int has_state = 0;
+
+	for (iter = ast_json_object_iter(json); iter; iter = ast_json_object_iter_next(json, iter)) {
+		if (strcmp("name", ast_json_object_iter_key(iter)) == 0) {
+			int prop_is_valid;
+			has_name = 1;
+			prop_is_valid = ast_ari_validate_string(
+				ast_json_object_iter_value(iter));
+			if (!prop_is_valid) {
+				ast_log(LOG_ERROR, "ARI DeviceState field name failed validation\n");
+				res = 0;
+			}
+		} else
+		if (strcmp("state", ast_json_object_iter_key(iter)) == 0) {
+			int prop_is_valid;
+			has_state = 1;
+			prop_is_valid = ast_ari_validate_string(
+				ast_json_object_iter_value(iter));
+			if (!prop_is_valid) {
+				ast_log(LOG_ERROR, "ARI DeviceState field state failed validation\n");
+				res = 0;
+			}
+		} else
+		{
+			ast_log(LOG_ERROR,
+				"ARI DeviceState has undocumented field %s\n",
+				ast_json_object_iter_key(iter));
+			res = 0;
+		}
+	}
+
+	if (!has_name) {
+		ast_log(LOG_ERROR, "ARI DeviceState missing required field name\n");
+		res = 0;
+	}
+
+	if (!has_state) {
+		ast_log(LOG_ERROR, "ARI DeviceState missing required field state\n");
+		res = 0;
+	}
+
+	return res;
+}
+
+ari_validator ast_ari_validate_device_state_fn(void)
+{
+	return ast_ari_validate_device_state;
+}
+
 int ast_ari_validate_application_replaced(struct ast_json *json)
 {
 	int res = 1;
@@ -2746,6 +2800,85 @@ ari_validator ast_ari_validate_channel_varset_fn(void)
 	return ast_ari_validate_channel_varset;
 }
 
+int ast_ari_validate_device_state_changed(struct ast_json *json)
+{
+	int res = 1;
+	struct ast_json_iter *iter;
+	int has_type = 0;
+	int has_application = 0;
+	int has_device_state = 0;
+
+	for (iter = ast_json_object_iter(json); iter; iter = ast_json_object_iter_next(json, iter)) {
+		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 DeviceStateChanged 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 DeviceStateChanged 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 DeviceStateChanged field timestamp failed validation\n");
+				res = 0;
+			}
+		} else
+		if (strcmp("device_state", ast_json_object_iter_key(iter)) == 0) {
+			int prop_is_valid;
+			has_device_state = 1;
+			prop_is_valid = ast_ari_validate_device_state(
+				ast_json_object_iter_value(iter));
+			if (!prop_is_valid) {
+				ast_log(LOG_ERROR, "ARI DeviceStateChanged field device_state failed validation\n");
+				res = 0;
+			}
+		} else
+		{
+			ast_log(LOG_ERROR,
+				"ARI DeviceStateChanged has undocumented field %s\n",
+				ast_json_object_iter_key(iter));
+			res = 0;
+		}
+	}
+
+	if (!has_type) {
+		ast_log(LOG_ERROR, "ARI DeviceStateChanged missing required field type\n");
+		res = 0;
+	}
+
+	if (!has_application) {
+		ast_log(LOG_ERROR, "ARI DeviceStateChanged missing required field application\n");
+		res = 0;
+	}
+
+	if (!has_device_state) {
+		ast_log(LOG_ERROR, "ARI DeviceStateChanged missing required field device_state\n");
+		res = 0;
+	}
+
+	return res;
+}
+
+ari_validator ast_ari_validate_device_state_changed_fn(void)
+{
+	return ast_ari_validate_device_state_changed;
+}
+
 int ast_ari_validate_endpoint_state_change(struct ast_json *json)
 {
 	int res = 1;
@@ -2887,6 +3020,9 @@ int ast_ari_validate_event(struct ast_json *json)
 	if (strcmp("ChannelVarset", discriminator) == 0) {
 		return ast_ari_validate_channel_varset(json);
 	} else
+	if (strcmp("DeviceStateChanged", discriminator) == 0) {
+		return ast_ari_validate_device_state_changed(json);
+	} else
 	if (strcmp("EndpointStateChange", discriminator) == 0) {
 		return ast_ari_validate_endpoint_state_change(json);
 	} else
@@ -3025,6 +3161,9 @@ int ast_ari_validate_message(struct ast_json *json)
 	if (strcmp("ChannelVarset", discriminator) == 0) {
 		return ast_ari_validate_channel_varset(json);
 	} else
+	if (strcmp("DeviceStateChanged", discriminator) == 0) {
+		return ast_ari_validate_device_state_changed(json);
+	} else
 	if (strcmp("EndpointStateChange", discriminator) == 0) {
 		return ast_ari_validate_endpoint_state_change(json);
 	} else
@@ -3592,6 +3731,7 @@ int ast_ari_validate_application(struct ast_json *json)
 	struct ast_json_iter *iter;
 	int has_bridge_ids = 0;
 	int has_channel_ids = 0;
+	int has_device_names = 0;
 	int has_endpoint_ids = 0;
 	int has_name = 0;
 
@@ -3618,6 +3758,17 @@ int ast_ari_validate_application(struct ast_json *json)
 				res = 0;
 			}
 		} else
+		if (strcmp("device_names", ast_json_object_iter_key(iter)) == 0) {
+			int prop_is_valid;
+			has_device_names = 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 Application field device_names failed validation\n");
+				res = 0;
+			}
+		} else
 		if (strcmp("endpoint_ids", ast_json_object_iter_key(iter)) == 0) {
 			int prop_is_valid;
 			has_endpoint_ids = 1;
@@ -3657,6 +3808,11 @@ int ast_ari_validate_application(struct ast_json *json)
 		res = 0;
 	}
 
+	if (!has_device_names) {
+		ast_log(LOG_ERROR, "ARI Application missing required field device_names\n");
+		res = 0;
+	}
+
 	if (!has_endpoint_ids) {
 		ast_log(LOG_ERROR, "ARI Application missing required field endpoint_ids\n");
 		res = 0;
diff --git a/res/ari/ari_model_validators.h b/res/ari/ari_model_validators.h
index a1e5b099e93a40e1f3493ba1d48892ef00558dcd..979fa4a47688001f403dc4af7b73e5a06e8f9ddc 100644
--- a/res/ari/ari_model_validators.h
+++ b/res/ari/ari_model_validators.h
@@ -480,6 +480,24 @@ int ast_ari_validate_playback(struct ast_json *json);
  */
 ari_validator ast_ari_validate_playback_fn(void);
 
+/*!
+ * \brief Validator for DeviceState.
+ *
+ * Represents the state of a device.
+ *
+ * \param json JSON object to validate.
+ * \returns True (non-zero) if valid.
+ * \returns False (zero) if invalid.
+ */
+int ast_ari_validate_device_state(struct ast_json *json);
+
+/*!
+ * \brief Function pointer to ast_ari_validate_device_state().
+ *
+ * See \ref ast_ari_model_validators.h for more details.
+ */
+ari_validator ast_ari_validate_device_state_fn(void);
+
 /*!
  * \brief Validator for ApplicationReplaced.
  *
@@ -754,6 +772,24 @@ int ast_ari_validate_channel_varset(struct ast_json *json);
  */
 ari_validator ast_ari_validate_channel_varset_fn(void);
 
+/*!
+ * \brief Validator for DeviceStateChanged.
+ *
+ * Notification that a device state has changed.
+ *
+ * \param json JSON object to validate.
+ * \returns True (non-zero) if valid.
+ * \returns False (zero) if invalid.
+ */
+int ast_ari_validate_device_state_changed(struct ast_json *json);
+
+/*!
+ * \brief Function pointer to ast_ari_validate_device_state_changed().
+ *
+ * See \ref ast_ari_model_validators.h for more details.
+ */
+ari_validator ast_ari_validate_device_state_changed_fn(void);
+
 /*!
  * \brief Validator for EndpointStateChange.
  *
@@ -1052,6 +1088,9 @@ ari_validator ast_ari_validate_application_fn(void);
  * - media_uri: string (required)
  * - state: string (required)
  * - target_uri: string (required)
+ * DeviceState
+ * - name: string (required)
+ * - state: string (required)
  * ApplicationReplaced
  * - type: string (required)
  * - application: string (required)
@@ -1143,6 +1182,11 @@ ari_validator ast_ari_validate_application_fn(void);
  * - channel: Channel
  * - value: string (required)
  * - variable: string (required)
+ * DeviceStateChanged
+ * - type: string (required)
+ * - application: string (required)
+ * - timestamp: Date
+ * - device_state: DeviceState (required)
  * EndpointStateChange
  * - type: string (required)
  * - application: string (required)
@@ -1187,6 +1231,7 @@ ari_validator ast_ari_validate_application_fn(void);
  * Application
  * - bridge_ids: List[string] (required)
  * - channel_ids: List[string] (required)
+ * - device_names: List[string] (required)
  * - endpoint_ids: List[string] (required)
  * - name: string (required)
  */
diff --git a/res/ari/resource_applications.h b/res/ari/resource_applications.h
index 12aacb3306face4fd8c25dfad0ecc35c37bd7928..6aebd07fd530bfb366fc4eaf6c2de3b3c46f5fd9 100644
--- a/res/ari/resource_applications.h
+++ b/res/ari/resource_applications.h
@@ -67,7 +67,7 @@ void ast_ari_applications_get(struct ast_variable *headers, struct ast_ari_appli
 struct ast_ari_applications_subscribe_args {
 	/*! \brief Application's name */
 	const char *application_name;
-	/*! \brief Array of URI for event source (channel:{channelId}, bridge:{bridgeId}, endpoint:{tech}/{resource} */
+	/*! \brief Array of URI for event source (channel:{channelId}, bridge:{bridgeId}, endpoint:{tech}/{resource}, deviceState:{deviceName} */
 	const char **event_source;
 	/*! \brief Length of event_source array. */
 	size_t event_source_count;
@@ -88,7 +88,7 @@ void ast_ari_applications_subscribe(struct ast_variable *headers, struct ast_ari
 struct ast_ari_applications_unsubscribe_args {
 	/*! \brief Application's name */
 	const char *application_name;
-	/*! \brief Array of URI for event source (channel:{channelId}, bridge:{bridgeId}, endpoint:{tech}/{resource} */
+	/*! \brief Array of URI for event source (channel:{channelId}, bridge:{bridgeId}, endpoint:{tech}/{resource}, device_state:{deviceName} */
 	const char **event_source;
 	/*! \brief Length of event_source array. */
 	size_t event_source_count;
diff --git a/res/ari/resource_device_states.c b/res/ari/resource_device_states.c
new file mode 100644
index 0000000000000000000000000000000000000000..50b87674028a2e729bcc190b4ddebe010b684110
--- /dev/null
+++ b/res/ari/resource_device_states.c
@@ -0,0 +1,111 @@
+/*
+ * Asterisk -- An open source telephony toolkit.
+ *
+ * Copyright (C) 2012 - 2013, Digium, Inc.
+ *
+ * Kevin Harwell <kharwell@digium.com>
+ *
+ * See http://www.asterisk.org for more information about
+ * the Asterisk project. Please do not directly contact
+ * any of the maintainers of this project for assistance;
+ * the project provides a web site, mailing lists and IRC
+ * channels for your use.
+ *
+ * This program is free software, distributed under the terms of
+ * the GNU General Public License Version 2. See the LICENSE file
+ * at the top of the source tree.
+ */
+
+/*! \file
+ *
+ * \brief /api-docs/deviceStates.{format} implementation- Device state resources
+ *
+ * \author Kevin Harwell <kharwell@digium.com>
+ */
+
+#include "asterisk.h"
+
+ASTERISK_FILE_VERSION(__FILE__, "$Revision$")
+
+#include "resource_device_states.h"
+#include "asterisk/stasis_app_device_state.h"
+
+void ast_ari_device_states_list(
+	struct ast_variable *headers,
+	struct ast_ari_device_states_list_args *args,
+	struct ast_ari_response *response)
+{
+	RAII_VAR(struct ast_json *, json, NULL, ast_json_unref);
+
+	if (!(json = stasis_app_device_states_to_json())) {
+		ast_ari_response_error(response, 500,
+			"Internal Server Error", "Error building response");
+		return;
+	}
+
+	ast_ari_response_ok(response, json);
+}
+
+void ast_ari_device_states_get(struct ast_variable *headers,
+	struct ast_ari_device_states_get_args *args,
+	struct ast_ari_response *response)
+{
+	RAII_VAR(struct ast_json *, json, NULL, ast_json_unref);
+
+	if (!(json = stasis_app_device_state_to_json(
+		      args->device_name, ast_device_state(args->device_name)))) {
+		ast_ari_response_error(response, 500,
+			"Internal Server Error", "Error building response");
+		return;
+	}
+
+	ast_ari_response_ok(response, json);
+}
+
+void ast_ari_device_states_update(struct ast_variable *headers,
+	struct ast_ari_device_states_update_args *args,
+	struct ast_ari_response *response)
+{
+	switch (stasis_app_device_state_update(
+			args->device_name, args->device_state)) {
+	case STASIS_DEVICE_STATE_NOT_CONTROLLED:
+		ast_ari_response_error(response, 409,
+			"Conflict", "Uncontrolled device specified");
+		return;
+	case STASIS_DEVICE_STATE_MISSING:
+		ast_ari_response_error(response, 404,
+			"Not Found", "Device name is missing");
+		return;
+	case STASIS_DEVICE_STATE_UNKNOWN:
+		ast_ari_response_error(response, 500, "Internal Server Error",
+				       "Unknown device");
+		return;
+	case STASIS_DEVICE_STATE_OK:
+	case STASIS_DEVICE_STATE_SUBSCRIBERS: /* shouldn't be returned for update */
+		ast_ari_response_no_content(response);
+	}
+}
+
+void ast_ari_device_states_delete(struct ast_variable *headers,
+	struct ast_ari_device_states_delete_args *args,
+	struct ast_ari_response *response)
+{
+	switch (stasis_app_device_state_delete(args->device_name)) {
+	case STASIS_DEVICE_STATE_NOT_CONTROLLED:
+		ast_ari_response_error(response, 409,
+			"Conflict", "Uncontrolled device specified");
+		return;
+	case STASIS_DEVICE_STATE_MISSING:
+		ast_ari_response_error(response, 404,
+			"Not Found", "Device name is missing");
+		return;
+	case STASIS_DEVICE_STATE_SUBSCRIBERS:
+		ast_ari_response_error(response, 500,
+			"Internal Server Error",
+			"Cannot delete device with subscribers");
+		return;
+	case STASIS_DEVICE_STATE_OK:
+	case STASIS_DEVICE_STATE_UNKNOWN:
+		ast_ari_response_no_content(response);
+	}
+}
diff --git a/res/ari/resource_device_states.h b/res/ari/resource_device_states.h
new file mode 100644
index 0000000000000000000000000000000000000000..a3bac999c1b489463c9cade4ecb998f2413dd3dc
--- /dev/null
+++ b/res/ari/resource_device_states.h
@@ -0,0 +1,95 @@
+/*
+ * Asterisk -- An open source telephony toolkit.
+ *
+ * Copyright (C) 2012 - 2013, Digium, Inc.
+ *
+ * Kevin Harwell <kharwell@digium.com>
+ *
+ * See http://www.asterisk.org for more information about
+ * the Asterisk project. Please do not directly contact
+ * any of the maintainers of this project for assistance;
+ * the project provides a web site, mailing lists and IRC
+ * channels for your use.
+ *
+ * This program is free software, distributed under the terms of
+ * the GNU General Public License Version 2. See the LICENSE file
+ * at the top of the source tree.
+ */
+
+/*! \file
+ *
+ * \brief Generated file - declares stubs to be implemented in
+ * res/ari/resource_deviceStates.c
+ *
+ * Device state resources
+ *
+ * \author Kevin Harwell <kharwell@digium.com>
+ */
+
+/*
+ * !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
+ * !!!!!                               DO NOT EDIT                        !!!!!
+ * !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
+ * This file is generated by a mustache template. Please see the original
+ * template in rest-api-templates/ari_resource.h.mustache
+ */
+
+#ifndef _ASTERISK_RESOURCE_DEVICESTATES_H
+#define _ASTERISK_RESOURCE_DEVICESTATES_H
+
+#include "asterisk/ari.h"
+
+/*! \brief Argument struct for ast_ari_device_states_list() */
+struct ast_ari_device_states_list_args {
+};
+/*!
+ * \brief List all ARI controlled device states.
+ *
+ * \param headers HTTP headers
+ * \param args Swagger parameters
+ * \param[out] response HTTP response
+ */
+void ast_ari_device_states_list(struct ast_variable *headers, struct ast_ari_device_states_list_args *args, struct ast_ari_response *response);
+/*! \brief Argument struct for ast_ari_device_states_get() */
+struct ast_ari_device_states_get_args {
+	/*! \brief Name of the device */
+	const char *device_name;
+};
+/*!
+ * \brief Retrieve the current state of a device.
+ *
+ * \param headers HTTP headers
+ * \param args Swagger parameters
+ * \param[out] response HTTP response
+ */
+void ast_ari_device_states_get(struct ast_variable *headers, struct ast_ari_device_states_get_args *args, struct ast_ari_response *response);
+/*! \brief Argument struct for ast_ari_device_states_update() */
+struct ast_ari_device_states_update_args {
+	/*! \brief Name of the device */
+	const char *device_name;
+	/*! \brief Device state value */
+	const char *device_state;
+};
+/*!
+ * \brief Change the state of a device controlled by ARI. (Note - implicitly creates the device state).
+ *
+ * \param headers HTTP headers
+ * \param args Swagger parameters
+ * \param[out] response HTTP response
+ */
+void ast_ari_device_states_update(struct ast_variable *headers, struct ast_ari_device_states_update_args *args, struct ast_ari_response *response);
+/*! \brief Argument struct for ast_ari_device_states_delete() */
+struct ast_ari_device_states_delete_args {
+	/*! \brief Name of the device */
+	const char *device_name;
+};
+/*!
+ * \brief Destroy a device-state controlled by ARI.
+ *
+ * \param headers HTTP headers
+ * \param args Swagger parameters
+ * \param[out] response HTTP response
+ */
+void ast_ari_device_states_delete(struct ast_variable *headers, struct ast_ari_device_states_delete_args *args, struct ast_ari_response *response);
+
+#endif /* _ASTERISK_RESOURCE_DEVICESTATES_H */
diff --git a/res/res_ari_device_states.c b/res/res_ari_device_states.c
new file mode 100644
index 0000000000000000000000000000000000000000..07f51ba3e96fdbdc1cf8ee7a914e9b8139d7c905
--- /dev/null
+++ b/res/res_ari_device_states.c
@@ -0,0 +1,323 @@
+/*
+ * Asterisk -- An open source telephony toolkit.
+ *
+ * Copyright (C) 2012 - 2013, Digium, Inc.
+ *
+ * Kevin Harwell <kharwell@digium.com>
+ *
+ * See http://www.asterisk.org for more information about
+ * the Asterisk project. Please do not directly contact
+ * any of the maintainers of this project for assistance;
+ * the project provides a web site, mailing lists and IRC
+ * channels for your use.
+ *
+ * This program is free software, distributed under the terms of
+ * the GNU General Public License Version 2. See the LICENSE file
+ * at the top of the source tree.
+ */
+
+/*
+ * !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
+ * !!!!!                               DO NOT EDIT                        !!!!!
+ * !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
+ * This file is generated by a mustache template. Please see the original
+ * template in rest-api-templates/res_ari_resource.c.mustache
+ */
+
+/*! \file
+ *
+ * \brief Device state resources
+ *
+ * \author Kevin Harwell <kharwell@digium.com>
+ */
+
+/*** MODULEINFO
+	<depend type="module">res_ari</depend>
+	<depend type="module">res_stasis</depend>
+	<support_level>core</support_level>
+ ***/
+
+#include "asterisk.h"
+
+ASTERISK_FILE_VERSION(__FILE__, "$Revision$")
+
+#include "asterisk/app.h"
+#include "asterisk/module.h"
+#include "asterisk/stasis_app.h"
+#include "ari/resource_device_states.h"
+#if defined(AST_DEVMODE)
+#include "ari/ari_model_validators.h"
+#endif
+
+#define MAX_VALS 128
+
+/*!
+ * \brief Parameter parsing callback for /deviceStates.
+ * \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_device_states_list_cb(
+	struct ast_variable *get_params, struct ast_variable *path_vars,
+	struct ast_variable *headers, struct ast_ari_response *response)
+{
+	struct ast_ari_device_states_list_args args = {};
+#if defined(AST_DEVMODE)
+	int is_valid;
+	int code;
+#endif /* AST_DEVMODE */
+
+	ast_ari_device_states_list(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 */
+		is_valid = 1;
+		break;
+	default:
+		if (200 <= code && code <= 299) {
+			is_valid = ast_ari_validate_list(response->message,
+				ast_ari_validate_device_state_fn());
+		} else {
+			ast_log(LOG_ERROR, "Invalid error response %d for /deviceStates\n", code);
+			is_valid = 0;
+		}
+	}
+
+	if (!is_valid) {
+		ast_log(LOG_ERROR, "Response validation failed for /deviceStates\n");
+		ast_ari_response_error(response, 500,
+			"Internal Server Error", "Response validation failed");
+	}
+#endif /* AST_DEVMODE */
+
+fin: __attribute__((unused))
+	return;
+}
+/*!
+ * \brief Parameter parsing callback for /deviceStates/{deviceName}.
+ * \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_device_states_get_cb(
+	struct ast_variable *get_params, struct ast_variable *path_vars,
+	struct ast_variable *headers, struct ast_ari_response *response)
+{
+	struct ast_ari_device_states_get_args args = {};
+	struct ast_variable *i;
+#if defined(AST_DEVMODE)
+	int is_valid;
+	int code;
+#endif /* AST_DEVMODE */
+
+	for (i = path_vars; i; i = i->next) {
+		if (strcmp(i->name, "deviceName") == 0) {
+			args.device_name = (i->value);
+		} else
+		{}
+	}
+	ast_ari_device_states_get(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 */
+		is_valid = 1;
+		break;
+	default:
+		if (200 <= code && code <= 299) {
+			is_valid = ast_ari_validate_device_state(
+				response->message);
+		} else {
+			ast_log(LOG_ERROR, "Invalid error response %d for /deviceStates/{deviceName}\n", code);
+			is_valid = 0;
+		}
+	}
+
+	if (!is_valid) {
+		ast_log(LOG_ERROR, "Response validation failed for /deviceStates/{deviceName}\n");
+		ast_ari_response_error(response, 500,
+			"Internal Server Error", "Response validation failed");
+	}
+#endif /* AST_DEVMODE */
+
+fin: __attribute__((unused))
+	return;
+}
+/*!
+ * \brief Parameter parsing callback for /deviceStates/{deviceName}.
+ * \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_device_states_update_cb(
+	struct ast_variable *get_params, struct ast_variable *path_vars,
+	struct ast_variable *headers, struct ast_ari_response *response)
+{
+	struct ast_ari_device_states_update_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, "deviceState") == 0) {
+			args.device_state = (i->value);
+		} else
+		{}
+	}
+	for (i = path_vars; i; i = i->next) {
+		if (strcmp(i->name, "deviceName") == 0) {
+			args.device_name = (i->value);
+		} else
+		{}
+	}
+	ast_ari_device_states_update(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: /* Device name is missing */
+	case 409: /* Uncontrolled device specified */
+		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 /deviceStates/{deviceName}\n", code);
+			is_valid = 0;
+		}
+	}
+
+	if (!is_valid) {
+		ast_log(LOG_ERROR, "Response validation failed for /deviceStates/{deviceName}\n");
+		ast_ari_response_error(response, 500,
+			"Internal Server Error", "Response validation failed");
+	}
+#endif /* AST_DEVMODE */
+
+fin: __attribute__((unused))
+	return;
+}
+/*!
+ * \brief Parameter parsing callback for /deviceStates/{deviceName}.
+ * \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_device_states_delete_cb(
+	struct ast_variable *get_params, struct ast_variable *path_vars,
+	struct ast_variable *headers, struct ast_ari_response *response)
+{
+	struct ast_ari_device_states_delete_args args = {};
+	struct ast_variable *i;
+#if defined(AST_DEVMODE)
+	int is_valid;
+	int code;
+#endif /* AST_DEVMODE */
+
+	for (i = path_vars; i; i = i->next) {
+		if (strcmp(i->name, "deviceName") == 0) {
+			args.device_name = (i->value);
+		} else
+		{}
+	}
+	ast_ari_device_states_delete(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: /* Device name is missing */
+	case 409: /* Uncontrolled device specified */
+		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 /deviceStates/{deviceName}\n", code);
+			is_valid = 0;
+		}
+	}
+
+	if (!is_valid) {
+		ast_log(LOG_ERROR, "Response validation failed for /deviceStates/{deviceName}\n");
+		ast_ari_response_error(response, 500,
+			"Internal Server Error", "Response validation failed");
+	}
+#endif /* AST_DEVMODE */
+
+fin: __attribute__((unused))
+	return;
+}
+
+/*! \brief REST handler for /api-docs/deviceStates.{format} */
+static struct stasis_rest_handlers deviceStates_deviceName = {
+	.path_segment = "deviceName",
+	.is_wildcard = 1,
+	.callbacks = {
+		[AST_HTTP_GET] = ast_ari_device_states_get_cb,
+		[AST_HTTP_PUT] = ast_ari_device_states_update_cb,
+		[AST_HTTP_DELETE] = ast_ari_device_states_delete_cb,
+	},
+	.num_children = 0,
+	.children = {  }
+};
+/*! \brief REST handler for /api-docs/deviceStates.{format} */
+static struct stasis_rest_handlers deviceStates = {
+	.path_segment = "deviceStates",
+	.callbacks = {
+		[AST_HTTP_GET] = ast_ari_device_states_list_cb,
+	},
+	.num_children = 1,
+	.children = { &deviceStates_deviceName, }
+};
+
+static int load_module(void)
+{
+	int res = 0;
+	stasis_app_ref();
+	res |= ast_ari_add_handler(&deviceStates);
+	return res;
+}
+
+static int unload_module(void)
+{
+	ast_ari_remove_handler(&deviceStates);
+	stasis_app_unref();
+	return 0;
+}
+
+AST_MODULE_INFO(ASTERISK_GPL_KEY, AST_MODFLAG_DEFAULT, "RESTful API module - Device state resources",
+	.load = load_module,
+	.unload = unload_module,
+	.nonoptreq = "res_ari,res_stasis",
+	);
diff --git a/res/res_stasis.c b/res/res_stasis.c
index e21941210b764c7cec19e81dbe3d62fdfac22b41..691462722e549eea34944b771ec83a2f41ce0000 100644
--- a/res/res_stasis.c
+++ b/res/res_stasis.c
@@ -103,10 +103,15 @@ struct ao2_container *app_bridges;
 
 struct ao2_container *app_bridges_moh;
 
+const char *stasis_app_name(const struct stasis_app *app)
+{
+	return app_name(app);
+}
+
 /*! AO2 hash function for \ref app */
 static int app_hash(const void *obj, const int flags)
 {
-	const struct app *app;
+	const struct stasis_app *app;
 	const char *key;
 
 	switch (flags & OBJ_SEARCH_MASK) {
@@ -115,7 +120,7 @@ static int app_hash(const void *obj, const int flags)
 		break;
 	case OBJ_SEARCH_OBJECT:
 		app = obj;
-		key = app_name(app);
+		key = stasis_app_name(app);
 		break;
 	default:
 		/* Hash can only work on something with a full key. */
@@ -128,24 +133,24 @@ static int app_hash(const void *obj, const int flags)
 /*! AO2 comparison function for \ref app */
 static int app_compare(void *obj, void *arg, int flags)
 {
-	const struct app *object_left = obj;
-	const struct app *object_right = arg;
+	const struct stasis_app *object_left = obj;
+	const struct stasis_app *object_right = arg;
 	const char *right_key = arg;
 	int cmp;
 
 	switch (flags & OBJ_SEARCH_MASK) {
 	case OBJ_SEARCH_OBJECT:
-		right_key = app_name(object_right);
+		right_key = stasis_app_name(object_right);
 		/* Fall through */
 	case OBJ_SEARCH_KEY:
-		cmp = strcmp(app_name(object_left), right_key);
+		cmp = strcmp(stasis_app_name(object_left), right_key);
 		break;
 	case OBJ_SEARCH_PARTIAL_KEY:
 		/*
 		 * We could also use a partial key struct containing a length
 		 * so strlen() does not get called for every comparison instead.
 		 */
-		cmp = strncmp(app_name(object_left), right_key, strlen(right_key));
+		cmp = strncmp(stasis_app_name(object_left), right_key, strlen(right_key));
 		break;
 	default:
 		/*
@@ -229,13 +234,13 @@ static int control_compare(void *obj, void *arg, int flags)
 
 static int cleanup_cb(void *obj, void *arg, int flags)
 {
-	struct app *app = obj;
+	struct stasis_app *app = obj;
 
 	if (!app_is_finished(app)) {
 		return 0;
 	}
 
-	ast_verb(1, "Shutting down application '%s'\n", app_name(app));
+	ast_verb(1, "Shutting down application '%s'\n", stasis_app_name(app));
 	app_shutdown(app);
 
 	return CMP_MATCH;
@@ -619,7 +624,7 @@ void stasis_app_bridge_destroy(const char *bridge_id)
 	ast_bridge_destroy(bridge, 0);
 }
 
-static int send_start_msg(struct app *app, struct ast_channel *chan,
+static int send_start_msg(struct stasis_app *app, struct ast_channel *chan,
 	int argc, char *argv[])
 {
 	RAII_VAR(struct ast_json *, msg, NULL, ast_json_unref);
@@ -667,7 +672,7 @@ static int send_start_msg(struct app *app, struct ast_channel *chan,
 	return 0;
 }
 
-static int send_end_msg(struct app *app, struct ast_channel *chan)
+static int send_end_msg(struct stasis_app *app, struct ast_channel *chan)
 {
 	RAII_VAR(struct ast_json *, msg, NULL, ast_json_unref);
 	RAII_VAR(struct ast_channel_snapshot *, snapshot, NULL, ao2_cleanup);
@@ -714,7 +719,7 @@ int stasis_app_exec(struct ast_channel *chan, const char *app_name, int argc,
 {
 	SCOPED_MODULE_USE(ast_module_info->self);
 
-	RAII_VAR(struct app *, app, NULL, ao2_cleanup);
+	RAII_VAR(struct stasis_app *, app, NULL, ao2_cleanup);
 	RAII_VAR(struct stasis_app_control *, control, NULL, control_unlink);
 	struct ast_bridge *last_bridge = NULL;
 	int res = 0;
@@ -838,7 +843,7 @@ int stasis_app_exec(struct ast_channel *chan, const char *app_name, int argc,
 
 int stasis_app_send(const char *app_name, struct ast_json *message)
 {
-	RAII_VAR(struct app *, app, NULL, ao2_cleanup);
+	RAII_VAR(struct stasis_app *, app, NULL, ao2_cleanup);
 
 	app = ao2_find(apps_registry, app_name, OBJ_SEARCH_KEY);
 	if (!app) {
@@ -849,17 +854,31 @@ int stasis_app_send(const char *app_name, struct ast_json *message)
 			"Stasis app '%s' not registered\n", app_name);
 		return -1;
 	}
-
 	app_send(app, message);
 	return 0;
 }
 
+static struct stasis_app *find_app_by_name(const char *app_name)
+{
+	struct stasis_app *res = NULL;
+
+	if (!ast_strlen_zero(app_name)) {
+		res = ao2_find(apps_registry, app_name, OBJ_SEARCH_KEY);
+	}
+
+	if (!res) {
+		ast_log(LOG_WARNING, "Could not find app '%s'\n",
+			app_name ? : "(null)");
+	}
+	return res;
+}
+
 static int append_name(void *obj, void *arg, int flags)
 {
-	struct app *app = obj;
+	struct stasis_app *app = obj;
 	struct ao2_container *apps = arg;
 
-	ast_str_container_add(apps, app_name(app));
+	ast_str_container_add(apps, stasis_app_name(app));
 	return 0;
 }
 
@@ -879,7 +898,7 @@ struct ao2_container *stasis_app_get_all(void)
 
 int stasis_app_register(const char *app_name, stasis_app_cb handler, void *data)
 {
-	RAII_VAR(struct app *, app, NULL, ao2_cleanup);
+	RAII_VAR(struct stasis_app *, app, NULL, ao2_cleanup);
 
 	SCOPED_LOCK(apps_lock, apps_registry, ao2_lock, ao2_unlock);
 
@@ -904,7 +923,7 @@ int stasis_app_register(const char *app_name, stasis_app_cb handler, void *data)
 
 void stasis_app_unregister(const char *app_name)
 {
-	RAII_VAR(struct app *, app, NULL, ao2_cleanup);
+	RAII_VAR(struct stasis_app *, app, NULL, ao2_cleanup);
 
 	if (!app_name) {
 		return;
@@ -925,217 +944,249 @@ void stasis_app_unregister(const char *app_name)
 	cleanup();
 }
 
-struct ast_json *stasis_app_to_json(const char *app_name)
+/*!
+ * \internal \brief List of registered event sources.
+ */
+AST_RWLIST_HEAD_STATIC(event_sources, stasis_app_event_source);
+
+void stasis_app_register_event_source(struct stasis_app_event_source *obj)
 {
-	RAII_VAR(struct app *, app, NULL, ao2_cleanup);
+	SCOPED_LOCK(lock, &event_sources, AST_RWLIST_WRLOCK, AST_RWLIST_UNLOCK);
+	AST_LIST_INSERT_TAIL(&event_sources, obj, next);
+	/* only need to bump the module ref on non-core sources because the
+	   core ones are [un]registered by this module. */
+	if (!stasis_app_is_core_event_source(obj)) {
+		ast_module_ref(ast_module_info->self);
+	}
+}
+
+void stasis_app_unregister_event_source(struct stasis_app_event_source *obj)
+{
+	struct stasis_app_event_source *source;
+	SCOPED_LOCK(lock, &event_sources, AST_RWLIST_WRLOCK, AST_RWLIST_UNLOCK);
+	AST_RWLIST_TRAVERSE_SAFE_BEGIN(&event_sources, source, next) {
+		if (source == obj) {
+			AST_RWLIST_REMOVE_CURRENT(next);
+			if (!stasis_app_is_core_event_source(obj)) {
+				ast_module_unref(ast_module_info->self);
+			}
+			break;
+		}
+	}
+	AST_RWLIST_TRAVERSE_SAFE_END;
+}
 
-	if (app_name) {
-		app = ao2_find(apps_registry, app_name, OBJ_SEARCH_KEY);
+/*!
+ * \internal
+ * \brief Convert event source data to JSON.
+ *
+ * Calls each event source that has a "to_json" handler allowing each
+ * source to add data to the given JSON object.
+ *
+ * \param app application associated with the event source
+ * \param json a json object to "fill"
+ *
+ * \retval The given json object.
+ */
+static struct ast_json *app_event_sources_to_json(
+	const struct stasis_app *app, struct ast_json *json)
+{
+	struct stasis_app_event_source *source;
+	SCOPED_LOCK(lock, &event_sources, AST_RWLIST_RDLOCK, AST_RWLIST_UNLOCK);
+	AST_LIST_TRAVERSE(&event_sources, source, next) {
+		if (source->to_json) {
+			source->to_json(app, json);
+		}
 	}
+	return json;
+}
 
+static struct ast_json *stasis_app_object_to_json(struct stasis_app *app)
+{
 	if (!app) {
 		return NULL;
 	}
 
-	return app_to_json(app);
+	return app_event_sources_to_json(app, app_to_json(app));
 }
 
-#define CHANNEL_SCHEME "channel:"
-#define BRIDGE_SCHEME "bridge:"
-#define ENDPOINT_SCHEME "endpoint:"
-
-/*! Struct for capturing event source information */
-struct event_source {
-	enum {
-		EVENT_SOURCE_CHANNEL,
-		EVENT_SOURCE_BRIDGE,
-		EVENT_SOURCE_ENDPOINT,
-	} event_source_type;
-	union {
-		struct ast_channel *channel;
-		struct ast_bridge *bridge;
-		struct ast_endpoint *endpoint;
-	};
-};
-
-enum stasis_app_subscribe_res stasis_app_subscribe(const char *app_name,
-	const char **event_source_uris, int event_sources_count,
-	struct ast_json **json)
+struct ast_json *stasis_app_to_json(const char *app_name)
 {
-	RAII_VAR(struct app *, app, NULL, ao2_cleanup);
-	RAII_VAR(struct event_source *, event_sources, NULL, ast_free);
-	enum stasis_app_subscribe_res res = STASIS_ASR_OK;
-	int i;
+	RAII_VAR(struct stasis_app *, app, find_app_by_name(app_name), ao2_cleanup);
 
-	if (app_name) {
-		app = ao2_find(apps_registry, app_name, OBJ_SEARCH_KEY);
+	return stasis_app_object_to_json(app);
+}
+
+/*!
+ * \internal
+ * \brief Finds an event source that matches a uri scheme.
+ *
+ * Uri(s) should begin with a particular scheme that can be matched
+ * against an event source.
+ *
+ * \param uri uri containing a scheme to match
+ *
+ * \retval an event source if found, NULL otherwise.
+ */
+static struct stasis_app_event_source *app_event_source_find(const char *uri)
+{
+	struct stasis_app_event_source *source;
+	SCOPED_LOCK(lock, &event_sources, AST_RWLIST_RDLOCK, AST_RWLIST_UNLOCK);
+	AST_LIST_TRAVERSE(&event_sources, source, next) {
+		if (ast_begins_with(uri, source->scheme)) {
+			return source;
+		}
 	}
+	return NULL;
+}
+
+/*!
+ * \internal
+ * \brief Callback for subscription handling
+ *
+ * \param app [un]subscribing application
+ * \param uri scheme:id of an event source
+ * \param event_source being [un]subscribed [from]to
+ *
+ * \retval stasis_app_subscribe_res return code.
+ */
+typedef enum stasis_app_subscribe_res (*app_subscription_handler)(
+	struct stasis_app *app, const char *uri,
+	struct stasis_app_event_source *event_source);
+
+/*!
+ * \internal
+ * \brief Subscriptions handler for application [un]subscribing.
+ *
+ * \param app_name Name of the application to subscribe.
+ * \param event_source_uris URIs for the event sources to subscribe to.
+ * \param event_sources_count Array size of event_source_uris.
+ * \param json Optional output pointer for JSON representation of the app
+ *             after adding the subscription.
+ * \param handler [un]subscribe handler
+ *
+ * \retval stasis_app_subscribe_res return code.
+ */
+static enum stasis_app_subscribe_res app_handle_subscriptions(
+	const char *app_name, const char **event_source_uris,
+	int event_sources_count, struct ast_json **json,
+	app_subscription_handler handler)
+{
+	RAII_VAR(struct stasis_app *, app, find_app_by_name(app_name), ao2_cleanup);
+	int i;
 
 	if (!app) {
-		ast_log(LOG_WARNING, "Could not find app '%s'\n",
-			app_name ? : "(null)");
 		return STASIS_ASR_APP_NOT_FOUND;
 	}
 
-	event_sources = ast_calloc(event_sources_count, sizeof(*event_sources));
-	if (!event_sources) {
-		return STASIS_ASR_INTERNAL_ERROR;
-	}
-
-	for (i = 0; res == STASIS_ASR_OK && i < event_sources_count; ++i) {
+	for (i = 0; i < event_sources_count; ++i) {
 		const char *uri = event_source_uris[i];
-		ast_debug(3, "%s: Checking %s\n", app_name,
-			uri);
-		if (ast_begins_with(uri, CHANNEL_SCHEME)) {
-			event_sources[i].event_source_type =
-				EVENT_SOURCE_CHANNEL;
-			event_sources[i].channel = ast_channel_get_by_name(
-				uri + strlen(CHANNEL_SCHEME));
-			if (!event_sources[i].channel) {
-				ast_log(LOG_WARNING, "Channel not found: %s\n", uri);
-				res = STASIS_ASR_EVENT_SOURCE_NOT_FOUND;
-			}
-		} else if (ast_begins_with(uri, BRIDGE_SCHEME)) {
-			event_sources[i].event_source_type =
-				EVENT_SOURCE_BRIDGE;
-			event_sources[i].bridge = stasis_app_bridge_find_by_id(
-				uri + strlen(BRIDGE_SCHEME));
-			if (!event_sources[i].bridge) {
-				ast_log(LOG_WARNING, "Bridge not found: %s\n", uri);
-				res = STASIS_ASR_EVENT_SOURCE_NOT_FOUND;
-			}
-		} else if (ast_begins_with(uri, ENDPOINT_SCHEME)) {
-			event_sources[i].event_source_type =
-				EVENT_SOURCE_ENDPOINT;
-			event_sources[i].endpoint = ast_endpoint_find_by_id(
-				uri + strlen(ENDPOINT_SCHEME));
-			if (!event_sources[i].endpoint) {
-				ast_log(LOG_WARNING, "Endpoint not found: %s\n", uri);
-				res = STASIS_ASR_EVENT_SOURCE_NOT_FOUND;
-			}
-		} else {
+		enum stasis_app_subscribe_res res = STASIS_ASR_INTERNAL_ERROR;
+		struct stasis_app_event_source *event_source;
+
+		if (!(event_source = app_event_source_find(uri))) {
 			ast_log(LOG_WARNING, "Invalid scheme: %s\n", uri);
-			res = STASIS_ASR_EVENT_SOURCE_BAD_SCHEME;
+			return STASIS_ASR_EVENT_SOURCE_BAD_SCHEME;
 		}
-	}
 
-	for (i = 0; res == STASIS_ASR_OK && i < event_sources_count; ++i) {
-		int sub_res = -1;
-		ast_debug(1, "%s: Subscribing to %s\n", app_name,
-			event_source_uris[i]);
-
-		switch (event_sources[i].event_source_type) {
-		case EVENT_SOURCE_CHANNEL:
-			sub_res = app_subscribe_channel(app,
-				event_sources[i].channel);
-			break;
-		case EVENT_SOURCE_BRIDGE:
-			sub_res = app_subscribe_bridge(app,
-				event_sources[i].bridge);
-			break;
-		case EVENT_SOURCE_ENDPOINT:
-			sub_res = app_subscribe_endpoint(app,
-				event_sources[i].endpoint);
-			break;
+		if (handler &&
+		    ((res = handler(app, uri, event_source)))) {
+			return res;
 		}
+	}
 
-		if (sub_res != 0) {
-			ast_log(LOG_WARNING,
-				"Error subscribing app '%s' to '%s'\n",
-				app_name, event_source_uris[i]);
-			res = STASIS_ASR_INTERNAL_ERROR;
-		}
+	if (json) {
+		ast_debug(3, "%s: Successful; setting results\n", app_name);
+		*json = stasis_app_object_to_json(app);
 	}
 
-	if (res == STASIS_ASR_OK && json) {
-		ast_debug(1, "%s: Successful; setting results\n", app_name);
-		*json = app_to_json(app);
+	return STASIS_ASR_OK;
+}
+
+/*!
+ * \internal
+ * \brief Subscribe an app to an event source.
+ *
+ * \param app subscribing application
+ * \param uri scheme:id of an event source
+ * \param event_source being subscribed to
+ *
+ * \retval stasis_app_subscribe_res return code.
+ */
+static enum stasis_app_subscribe_res app_subscribe(
+	struct stasis_app *app, const char *uri,
+	struct stasis_app_event_source *event_source)
+{
+	const char *app_name = stasis_app_name(app);
+	RAII_VAR(void *, obj, NULL, ao2_cleanup);
+
+	ast_debug(3, "%s: Checking %s\n", app_name, uri);
+
+	if (!event_source->find ||
+	    (!(obj = event_source->find(app, uri + strlen(event_source->scheme))))) {
+		ast_log(LOG_WARNING, "Event source not found: %s\n", uri);
+		return STASIS_ASR_EVENT_SOURCE_NOT_FOUND;
 	}
 
-	for (i = 0; i < event_sources_count; ++i) {
-		switch (event_sources[i].event_source_type) {
-		case EVENT_SOURCE_CHANNEL:
-			event_sources[i].channel =
-				ast_channel_cleanup(event_sources[i].channel);
-			break;
-		case EVENT_SOURCE_BRIDGE:
-			ao2_cleanup(event_sources[i].bridge);
-			event_sources[i].bridge = NULL;
-			break;
-		case EVENT_SOURCE_ENDPOINT:
-			ao2_cleanup(event_sources[i].endpoint);
-			event_sources[i].endpoint = NULL;
-			break;
-		}
+	ast_debug(3, "%s: Subscribing to %s\n", app_name, uri);
+
+	if (!event_source->subscribe || (event_source->subscribe(app, obj))) {
+		ast_log(LOG_WARNING, "Error subscribing app '%s' to '%s'\n",
+			app_name, uri);
+		return STASIS_ASR_INTERNAL_ERROR;
 	}
 
-	return res;
+	return STASIS_ASR_OK;
 }
 
-enum stasis_app_subscribe_res stasis_app_unsubscribe(const char *app_name,
+enum stasis_app_subscribe_res stasis_app_subscribe(const char *app_name,
 	const char **event_source_uris, int event_sources_count,
 	struct ast_json **json)
 {
-	RAII_VAR(struct app *, app, NULL, ao2_cleanup);
-	enum stasis_app_subscribe_res res = STASIS_ASR_OK;
-	int i;
+	return app_handle_subscriptions(
+		app_name, event_source_uris, event_sources_count,
+		json, app_subscribe);
+}
 
-	if (app_name) {
-		app = ao2_find(apps_registry, app_name, OBJ_SEARCH_KEY);
-	}
+/*!
+ * \internal
+ * \brief Unsubscribe an app from an event source.
+ *
+ * \param app application to unsubscribe
+ * \param uri scheme:id of an event source
+ * \param event_source being unsubscribed from
+ *
+ * \retval stasis_app_subscribe_res return code.
+ */
+static enum stasis_app_subscribe_res app_unsubscribe(
+	struct stasis_app *app, const char *uri,
+	struct stasis_app_event_source *event_source)
+{
+	const char *app_name = stasis_app_name(app);
+	const char *id = uri + strlen(event_source->scheme);
 
-	if (!app) {
-		ast_log(LOG_WARNING, "Could not find app '%s'\n",
-			app_name ? : "(null)");
-		return STASIS_ASR_APP_NOT_FOUND;
+	if (!event_source->is_subscribed ||
+	    (!event_source->is_subscribed(app, id))) {
+		return STASIS_ASR_EVENT_SOURCE_NOT_FOUND;
 	}
 
-	/* Validate the input */
-	for (i = 0; res == STASIS_ASR_OK && i < event_sources_count; ++i) {
-		if (ast_begins_with(event_source_uris[i], CHANNEL_SCHEME)) {
-			const char *channel_id = event_source_uris[i] +
-				strlen(CHANNEL_SCHEME);
-			if (!app_is_subscribed_channel_id(app, channel_id)) {
-				res = STASIS_ASR_EVENT_SOURCE_NOT_FOUND;
-			}
-		} else if (ast_begins_with(event_source_uris[i], BRIDGE_SCHEME)) {
-			const char *bridge_id = event_source_uris[i] +
-				strlen(BRIDGE_SCHEME);
-			if (!app_is_subscribed_bridge_id(app, bridge_id)) {
-				res = STASIS_ASR_EVENT_SOURCE_NOT_FOUND;
-			}
-		} else if (ast_begins_with(event_source_uris[i], ENDPOINT_SCHEME)) {
-			const char *endpoint_id = event_source_uris[i] +
-				strlen(ENDPOINT_SCHEME);
-			if (!app_is_subscribed_endpoint_id(app, endpoint_id)) {
-				res = STASIS_ASR_EVENT_SOURCE_NOT_FOUND;
-			}
-		} else {
-			res = STASIS_ASR_EVENT_SOURCE_BAD_SCHEME;
-		}
-	}
+	ast_debug(3, "%s: Unsubscribing from %s\n", app_name, uri);
 
-	for (i = 0; res == STASIS_ASR_OK && i < event_sources_count; ++i) {
-		if (ast_begins_with(event_source_uris[i], CHANNEL_SCHEME)) {
-			const char *channel_id = event_source_uris[i] +
-				strlen(CHANNEL_SCHEME);
-			app_unsubscribe_channel_id(app, channel_id);
-		} else if (ast_begins_with(event_source_uris[i], BRIDGE_SCHEME)) {
-			const char *bridge_id = event_source_uris[i] +
-				strlen(BRIDGE_SCHEME);
-			app_unsubscribe_bridge_id(app, bridge_id);
-		} else if (ast_begins_with(event_source_uris[i], ENDPOINT_SCHEME)) {
-			const char *endpoint_id = event_source_uris[i] +
-				strlen(ENDPOINT_SCHEME);
-			app_unsubscribe_endpoint_id(app, endpoint_id);
-		}
-	}
-
-	if (res == STASIS_ASR_OK && json) {
-		*json = app_to_json(app);
+	if (!event_source->unsubscribe || (event_source->unsubscribe(app, id))) {
+		ast_log(LOG_WARNING, "Error unsubscribing app '%s' to '%s'\n",
+			app_name, uri);
+		return -1;
 	}
+	return 0;
+}
 
-	return res;
+enum stasis_app_subscribe_res stasis_app_unsubscribe(const char *app_name,
+	const char **event_source_uris, int event_sources_count,
+	struct ast_json **json)
+{
+	return app_handle_subscriptions(
+		app_name, event_source_uris, event_sources_count,
+		json, app_unsubscribe);
 }
 
 void stasis_app_ref(void)
@@ -1150,6 +1201,8 @@ void stasis_app_unref(void)
 
 static int unload_module(void)
 {
+	stasis_app_unregister_event_sources();
+
 	ao2_cleanup(apps_registry);
 	apps_registry = NULL;
 
@@ -1206,6 +1259,8 @@ static int load_module(void)
 		return AST_MODULE_LOAD_FAILURE;
 	}
 
+	stasis_app_register_event_sources();
+
 	return AST_MODULE_LOAD_SUCCESS;
 }
 
diff --git a/res/res_stasis_device_state.c b/res/res_stasis_device_state.c
new file mode 100644
index 0000000000000000000000000000000000000000..e6d339107ca409081c96cba9a91756ff72ece0df
--- /dev/null
+++ b/res/res_stasis_device_state.c
@@ -0,0 +1,416 @@
+/*
+ * Asterisk -- An open source telephony toolkit.
+ *
+ * Copyright (C) 2013, Digium, Inc.
+ *
+ * Kevin Harwell <kharwell@digium.com>
+ *
+ * See http://www.asterisk.org for more information about
+ * the Asterisk project. Please do not directly contact
+ * any of the maintainers of this project for assistance;
+ * the project provides a web site, mailing lists and IRC
+ * channels for your use.
+ *
+ * This program is free software, distributed under the terms of
+ * the GNU General Public License Version 2. See the LICENSE file
+ * at the top of the source tree.
+ */
+
+/*** MODULEINFO
+	<depend type="module">res_stasis</depend>
+	<support_level>core</support_level>
+ ***/
+
+#include "asterisk.h"
+
+ASTERISK_FILE_VERSION(__FILE__, "$Revision$")
+
+#include "asterisk/astdb.h"
+#include "asterisk/astobj2.h"
+#include "asterisk/module.h"
+#include "asterisk/stasis_app_impl.h"
+#include "asterisk/stasis_app_device_state.h"
+
+#define DEVICE_STATE_SIZE 64
+/*! astdb family name */
+#define DEVICE_STATE_FAMILY "StasisDeviceState"
+/*! Stasis device state provider */
+#define DEVICE_STATE_PROVIDER_STASIS "Stasis"
+/*! Scheme for custom device states */
+#define DEVICE_STATE_SCHEME_STASIS "Stasis:"
+/*! Scheme for device state subscriptions */
+#define DEVICE_STATE_SCHEME_SUB "device_state:"
+
+/*! Number of hash buckets for device state subscriptions */
+#define DEVICE_STATE_BUCKETS 37
+
+/*! Container for subscribed device states */
+static struct ao2_container *device_state_subscriptions;
+
+/*!
+ * \brief Device state subscription object.
+ */
+struct device_state_subscription {
+	AST_DECLARE_STRING_FIELDS(
+		AST_STRING_FIELD(app_name);
+		AST_STRING_FIELD(device_name);
+	);
+	/*! The subscription object */
+	struct stasis_subscription *sub;
+};
+
+static int device_state_subscriptions_hash(const void *obj, const int flags)
+{
+	const struct device_state_subscription *object;
+
+	switch (flags & OBJ_SEARCH_MASK) {
+	case OBJ_SEARCH_OBJECT:
+		object = obj;
+		return ast_str_hash(object->device_name);
+	case OBJ_SEARCH_KEY:
+	default:
+		/* Hash can only work on something with a full key. */
+		ast_assert(0);
+		return 0;
+	}
+}
+
+static int device_state_subscriptions_cmp(void *obj, void *arg, int flags)
+{
+	const struct device_state_subscription *object_left = obj;
+	const struct device_state_subscription *object_right = arg;
+	int cmp;
+
+	switch (flags & OBJ_SEARCH_MASK) {
+	case OBJ_SEARCH_OBJECT:
+		/* find objects matching both device and app names */
+		if (strcmp(object_left->device_name,
+			   object_right->device_name)) {
+			return 0;
+		}
+		cmp = strcmp(object_left->app_name, object_right->app_name);
+		break;
+	case OBJ_SEARCH_KEY:
+	case OBJ_SEARCH_PARTIAL_KEY:
+		ast_assert(0); /* not supported by container */
+		/* fall through */
+	default:
+		cmp = 0;
+		break;
+	}
+
+	return cmp ? 0 : CMP_MATCH | CMP_STOP;
+}
+
+static void device_state_subscription_destroy(void *obj)
+{
+	struct device_state_subscription *sub = obj;
+	sub->sub = stasis_unsubscribe(sub->sub);
+	ast_string_field_free_memory(sub);
+}
+
+static struct device_state_subscription *device_state_subscription_create(
+	const struct stasis_app *app, const char *device_name)
+{
+	struct device_state_subscription *sub = ao2_alloc(
+		sizeof(*sub), device_state_subscription_destroy);
+	const char *app_name = stasis_app_name(app);
+	size_t size = strlen(device_name) + strlen(app_name) + 2;
+
+	if (!sub) {
+		return NULL;
+	}
+
+	if (ast_string_field_init(sub, size)) {
+		ao2_ref(sub, -1);
+		return NULL;
+	}
+
+	ast_string_field_set(sub, app_name, app_name);
+	ast_string_field_set(sub, device_name, device_name);
+	return sub;
+}
+
+static struct device_state_subscription *find_device_state_subscription(
+	struct stasis_app *app, const char *name)
+{
+	struct device_state_subscription dummy_sub = {
+		.app_name = stasis_app_name(app),
+		.device_name = name
+	};
+
+	return ao2_find(device_state_subscriptions, &dummy_sub, OBJ_SEARCH_OBJECT);
+}
+
+static void remove_device_state_subscription(
+	struct device_state_subscription *sub)
+{
+	ao2_unlink(device_state_subscriptions, sub);
+}
+
+struct ast_json *stasis_app_device_state_to_json(
+	const char *name, enum ast_device_state state)
+{
+	return ast_json_pack("{s: s, s: s}",
+			     "name", name,
+			     "state", ast_devstate_str(state));
+}
+
+struct ast_json *stasis_app_device_states_to_json(void)
+{
+	struct ast_json *array = ast_json_array_create();
+	RAII_VAR(struct ast_db_entry *, tree,
+		 ast_db_gettree(DEVICE_STATE_FAMILY, NULL), ast_db_freetree);
+	struct ast_db_entry *entry;
+
+	for (entry = tree; entry; entry = entry->next) {
+		const char *name = strrchr(entry->key, '/');
+		if (!ast_strlen_zero(name)) {
+			struct ast_str *device = ast_str_alloca(DEVICE_STATE_SIZE);
+			ast_str_set(&device, 0, "%s%s",
+				    DEVICE_STATE_SCHEME_STASIS, ++name);
+			ast_json_array_append(
+				array, stasis_app_device_state_to_json(
+					ast_str_buffer(device),
+					ast_device_state(ast_str_buffer(device))));
+		}
+	}
+
+	return array;
+}
+
+static void send_device_state(struct device_state_subscription *sub,
+			      const char *name, enum ast_device_state state)
+{
+	RAII_VAR(struct ast_json *, json, NULL, ast_json_unref);
+
+	json = ast_json_pack("{s:s, s:s, s:o, s:o}",
+			     "type", "DeviceStateChanged",
+			     "application", sub->app_name,
+			     "timestamp", ast_json_timeval(ast_tvnow(), NULL),
+			     "device_state", stasis_app_device_state_to_json(
+				     name, state));
+
+	if (!json) {
+		ast_log(LOG_ERROR, "Unable to create device state json object\n");
+		return;
+	}
+
+	stasis_app_send(sub->app_name, json);
+}
+
+enum stasis_device_state_result stasis_app_device_state_update(
+	const char *name, const char *value)
+{
+	size_t size = strlen(DEVICE_STATE_SCHEME_STASIS);
+	enum ast_device_state state;
+
+	ast_debug(3, "Updating device name = %s, value = %s", name, value);
+
+	if (strncasecmp(name, DEVICE_STATE_SCHEME_STASIS, size)) {
+		ast_log(LOG_ERROR, "Update can only be used to set "
+			"'%s' device state!\n", DEVICE_STATE_SCHEME_STASIS);
+		return STASIS_DEVICE_STATE_NOT_CONTROLLED;
+	}
+
+	name += size;
+	if (ast_strlen_zero(name)) {
+		ast_log(LOG_ERROR, "Update requires custom device name!\n");
+		return STASIS_DEVICE_STATE_MISSING;
+	}
+
+	if (!value || (state = ast_devstate_val(value)) == AST_DEVICE_UNKNOWN) {
+		ast_log(LOG_ERROR, "Unknown device state "
+			"value '%s'\n", value);
+		return STASIS_DEVICE_STATE_UNKNOWN;
+	}
+
+	ast_db_put(DEVICE_STATE_FAMILY, name, value);
+	ast_devstate_changed(state, AST_DEVSTATE_CACHABLE, "%s%s",
+			     DEVICE_STATE_SCHEME_STASIS, name);
+
+	return STASIS_DEVICE_STATE_OK;
+}
+
+enum stasis_device_state_result stasis_app_device_state_delete(const char *name)
+{
+	const char *full_name = name;
+	size_t size = strlen(DEVICE_STATE_SCHEME_STASIS);
+
+	if (strncasecmp(name, DEVICE_STATE_SCHEME_STASIS, size)) {
+		ast_log(LOG_ERROR, "Can only delete '%s' device states!\n",
+			DEVICE_STATE_SCHEME_STASIS);
+		return STASIS_DEVICE_STATE_NOT_CONTROLLED;
+	}
+
+	name += size;
+	if (ast_strlen_zero(name)) {
+		ast_log(LOG_ERROR, "Delete requires a device name!\n");
+		return STASIS_DEVICE_STATE_MISSING;
+	}
+
+	if (ast_device_state_clear_cache(full_name)) {
+		return STASIS_DEVICE_STATE_UNKNOWN;
+	}
+
+	ast_db_del(DEVICE_STATE_FAMILY, name);
+
+	/* send state change for delete */
+	ast_devstate_changed(
+		AST_DEVICE_UNKNOWN, AST_DEVSTATE_CACHABLE, "%s%s",
+		DEVICE_STATE_SCHEME_STASIS, name);
+
+	return STASIS_DEVICE_STATE_OK;
+}
+
+static void populate_cache(void)
+{
+	RAII_VAR(struct ast_db_entry *, tree,
+		 ast_db_gettree(DEVICE_STATE_FAMILY, NULL), ast_db_freetree);
+	struct ast_db_entry *entry;
+
+	for (entry = tree; entry; entry = entry->next) {
+		const char *name = strrchr(entry->key, '/');
+		if (!ast_strlen_zero(name)) {
+			ast_devstate_changed(
+				ast_devstate_val(entry->data),
+				AST_DEVSTATE_CACHABLE, "%s%s\n",
+				DEVICE_STATE_SCHEME_STASIS, name + 1);
+		}
+	}
+}
+
+static enum ast_device_state stasis_device_state_cb(const char *data)
+{
+	char buf[DEVICE_STATE_SIZE] = "";
+
+	ast_db_get(DEVICE_STATE_FAMILY, data, buf, sizeof(buf));
+
+	return ast_devstate_val(buf);
+}
+
+static void device_state_cb(void *data, struct stasis_subscription *sub,
+			    struct stasis_message *msg)
+{
+	struct ast_device_state_message *device_state;
+
+	if (ast_device_state_message_type() != stasis_message_type(msg)) {
+		return;
+	}
+
+	device_state = stasis_message_data(msg);
+	if (device_state->eid) {
+		/* ignore non-aggregate states */
+		return;
+	}
+
+	send_device_state(data, device_state->device, device_state->state);
+}
+
+static void *find_device_state(const struct stasis_app *app, const char *name)
+{
+	return device_state_subscription_create(app, name);
+}
+
+static int is_subscribed_device_state(struct stasis_app *app, const char *name)
+{
+	RAII_VAR(struct device_state_subscription *, sub,
+		 find_device_state_subscription(app, name), ao2_cleanup);
+	return sub != NULL;
+}
+
+static int subscribe_device_state(struct stasis_app *app, void *obj)
+{
+	struct device_state_subscription *sub = obj;
+
+	ast_debug(3, "Subscribing to device %s", sub->device_name);
+
+	if (is_subscribed_device_state(app, sub->device_name)) {
+		ast_log(LOG_WARNING, "Already subscribed to %s\n", sub->device_name);
+		return -1;
+	}
+
+	if (!(sub->sub = stasis_subscribe(
+			ast_device_state_topic(sub->device_name),
+			device_state_cb, sub))) {
+		ast_log(LOG_ERROR, "Unable to subscribe to device %s\n",
+			sub->device_name);
+		return -1;
+	}
+
+	ao2_link(device_state_subscriptions, sub);
+	return 0;
+}
+
+static int unsubscribe_device_state(struct stasis_app *app, const char *name)
+{
+	RAII_VAR(struct device_state_subscription *, sub,
+		 find_device_state_subscription(app, name), ao2_cleanup);
+	remove_device_state_subscription(sub);
+	return 0;
+}
+
+static int device_to_json_cb(void *obj, void *arg, void *data, int flags)
+{
+	struct device_state_subscription *sub = obj;
+	const char *app_name = arg;
+	struct ast_json *array = data;
+
+	if (strcmp(sub->app_name, app_name)) {
+		return 0;
+	}
+
+	ast_json_array_append(
+		array, ast_json_string_create(sub->device_name));
+	return 0;
+
+}
+
+static void devices_to_json(const struct stasis_app *app, struct ast_json *json)
+{
+	struct ast_json *array = ast_json_array_create();
+	ao2_callback_data(device_state_subscriptions, OBJ_NODATA,
+			  device_to_json_cb, (void *)stasis_app_name(app), array);
+	ast_json_object_set(json, "device_names", array);
+}
+
+struct stasis_app_event_source device_state_event_source = {
+	.scheme = DEVICE_STATE_SCHEME_SUB,
+	.find = find_device_state,
+	.subscribe = subscribe_device_state,
+	.unsubscribe = unsubscribe_device_state,
+	.is_subscribed = is_subscribed_device_state,
+	.to_json = devices_to_json
+};
+
+static int load_module(void)
+{
+	populate_cache();
+	if (ast_devstate_prov_add(DEVICE_STATE_PROVIDER_STASIS,
+				  stasis_device_state_cb)) {
+		return AST_MODULE_LOAD_FAILURE;
+	}
+
+	if (!(device_state_subscriptions = ao2_container_alloc(
+		      DEVICE_STATE_BUCKETS, device_state_subscriptions_hash,
+		      device_state_subscriptions_cmp))) {
+		return AST_MODULE_LOAD_FAILURE;
+	}
+
+	stasis_app_register_event_source(&device_state_event_source);
+	return AST_MODULE_LOAD_SUCCESS;
+}
+
+static int unload_module(void)
+{
+	ast_devstate_prov_del(DEVICE_STATE_PROVIDER_STASIS);
+	stasis_app_unregister_event_source(&device_state_event_source);
+	ao2_cleanup(device_state_subscriptions);
+	device_state_subscriptions = NULL;
+	return 0;
+}
+
+AST_MODULE_INFO(ASTERISK_GPL_KEY, AST_MODFLAG_GLOBAL_SYMBOLS, "Stasis application device state support",
+	.load = load_module,
+	.unload = unload_module,
+	.nonoptreq = "res_stasis");
diff --git a/res/res_stasis_device_state.exports.in b/res/res_stasis_device_state.exports.in
new file mode 100644
index 0000000000000000000000000000000000000000..0ad493c49ec08d12ba952400b7fb25a40a8dcd98
--- /dev/null
+++ b/res/res_stasis_device_state.exports.in
@@ -0,0 +1,6 @@
+{
+	global:
+		LINKER_SYMBOL_PREFIXstasis_app_*;
+	local:
+		*;
+};
diff --git a/res/stasis/app.c b/res/stasis/app.c
index 433d3adb5324a8dcc6f63d65f593e0f9f32368aa..8ad41e565bae337b109a375b305c7bd201b1c4b0 100644
--- a/res/stasis/app.c
+++ b/res/stasis/app.c
@@ -36,7 +36,7 @@ ASTERISK_FILE_VERSION(__FILE__, "$Revision$")
 #include "asterisk/stasis_endpoints.h"
 #include "asterisk/stasis_message_router.h"
 
-struct app {
+struct stasis_app {
 	/*! Aggregation topic for this application. */
 	struct stasis_topic *topic;
 	/*! Router for handling messages forwarded to \a topic. */
@@ -93,7 +93,7 @@ static void forwards_unsubscribe(struct app_forwards *forwards)
 	forwards->topic_cached_forward = NULL;
 }
 
-static struct app_forwards *forwards_create(struct app *app,
+static struct app_forwards *forwards_create(struct stasis_app *app,
 	const char *id)
 {
 	RAII_VAR(struct app_forwards *, forwards, NULL, ao2_cleanup);
@@ -114,7 +114,7 @@ static struct app_forwards *forwards_create(struct app *app,
 }
 
 /*! Forward a channel's topics to an app */
-static struct app_forwards *forwards_create_channel(struct app *app,
+static struct app_forwards *forwards_create_channel(struct stasis_app *app,
 	struct ast_channel *chan)
 {
 	RAII_VAR(struct app_forwards *, forwards, NULL, ao2_cleanup);
@@ -149,7 +149,7 @@ static struct app_forwards *forwards_create_channel(struct app *app,
 }
 
 /*! Forward a bridge's topics to an app */
-static struct app_forwards *forwards_create_bridge(struct app *app,
+static struct app_forwards *forwards_create_bridge(struct stasis_app *app,
 	struct ast_bridge *bridge)
 {
 	RAII_VAR(struct app_forwards *, forwards, NULL, ao2_cleanup);
@@ -184,7 +184,7 @@ static struct app_forwards *forwards_create_bridge(struct app *app,
 }
 
 /*! Forward a endpoint's topics to an app */
-static struct app_forwards *forwards_create_endpoint(struct app *app,
+static struct app_forwards *forwards_create_endpoint(struct stasis_app *app,
 	struct ast_endpoint *endpoint)
 {
 	RAII_VAR(struct app_forwards *, forwards, NULL, ao2_cleanup);
@@ -250,7 +250,7 @@ static int forwards_sort(const void *obj_left, const void *obj_right, int flags)
 
 static void app_dtor(void *obj)
 {
-	struct app *app = obj;
+	struct stasis_app *app = obj;
 
 	ast_verb(1, "Destroying Stasis app %s\n", app->name);
 
@@ -268,7 +268,7 @@ static void app_dtor(void *obj)
 static void sub_default_handler(void *data, struct stasis_subscription *sub,
 	struct stasis_message *message)
 {
-	struct app *app = data;
+	struct stasis_app *app = data;
 	RAII_VAR(struct ast_json *, json, NULL, ast_json_unref);
 
 	if (stasis_subscription_final_message(sub, message)) {
@@ -435,7 +435,7 @@ static void sub_channel_update_handler(void *data,
 	struct stasis_subscription *sub,
 	struct stasis_message *message)
 {
-	struct app *app = data;
+	struct stasis_app *app = data;
 	struct stasis_cache_update *update;
 	struct ast_channel_snapshot *new_snapshot;
 	struct ast_channel_snapshot *old_snapshot;
@@ -489,7 +489,7 @@ static void sub_endpoint_update_handler(void *data,
 	struct stasis_message *message)
 {
 	RAII_VAR(struct ast_json *, json, NULL, ast_json_unref);
-	struct app *app = data;
+	struct stasis_app *app = data;
 	struct stasis_cache_update *update;
 	struct ast_endpoint_snapshot *new_snapshot;
 	const struct timeval *tv;
@@ -535,7 +535,7 @@ static void sub_bridge_update_handler(void *data,
 	struct stasis_message *message)
 {
 	RAII_VAR(struct ast_json *, json, NULL, ast_json_unref);
-	struct app *app = data;
+	struct stasis_app *app = data;
 	struct stasis_cache_update *update;
 	struct ast_bridge_snapshot *new_snapshot;
 	struct ast_bridge_snapshot *old_snapshot;
@@ -569,7 +569,7 @@ static void sub_bridge_update_handler(void *data,
 static void bridge_merge_handler(void *data, struct stasis_subscription *sub,
 	struct stasis_message *message)
 {
-	struct app *app = data;
+	struct stasis_app *app = data;
 	struct ast_bridge_merge_message *merge;
 	RAII_VAR(struct app_forwards *, forwards, NULL, ao2_cleanup);
 
@@ -599,9 +599,9 @@ static void bridge_merge_handler(void *data, struct stasis_subscription *sub,
 	stasis_publish(app->topic, message);
 }
 
-struct app *app_create(const char *name, stasis_app_cb handler, void *data)
+struct stasis_app *app_create(const char *name, stasis_app_cb handler, void *data)
 {
-	RAII_VAR(struct app *, app, NULL, ao2_cleanup);
+	RAII_VAR(struct stasis_app *, app, NULL, ao2_cleanup);
 	size_t size;
 	int res = 0;
 
@@ -674,7 +674,7 @@ struct app *app_create(const char *name, stasis_app_cb handler, void *data)
  * \param app App to send the message to.
  * \param message Message to send.
  */
-void app_send(struct app *app, struct ast_json *message)
+void app_send(struct stasis_app *app, struct ast_json *message)
 {
 	stasis_app_cb handler;
 	RAII_VAR(void *, data, NULL, ao2_cleanup);
@@ -699,7 +699,7 @@ void app_send(struct app *app, struct ast_json *message)
 	handler(data, app->name, message);
 }
 
-void app_deactivate(struct app *app)
+void app_deactivate(struct stasis_app *app)
 {
 	SCOPED_AO2LOCK(lock, app);
 	ast_verb(1, "Deactivating Stasis app '%s'\n", app->name);
@@ -708,7 +708,7 @@ void app_deactivate(struct app *app)
 	app->data = NULL;
 }
 
-void app_shutdown(struct app *app)
+void app_shutdown(struct stasis_app *app)
 {
 	SCOPED_AO2LOCK(lock, app);
 
@@ -720,20 +720,20 @@ void app_shutdown(struct app *app)
 	app->bridge_merge_sub = NULL;
 }
 
-int app_is_active(struct app *app)
+int app_is_active(struct stasis_app *app)
 {
 	SCOPED_AO2LOCK(lock, app);
 	return app->handler != NULL;
 }
 
-int app_is_finished(struct app *app)
+int app_is_finished(struct stasis_app *app)
 {
 	SCOPED_AO2LOCK(lock, app);
 
 	return app->handler == NULL && ao2_container_count(app->forwards) == 0;
 }
 
-void app_update(struct app *app, stasis_app_cb handler, void *data)
+void app_update(struct stasis_app *app, stasis_app_cb handler, void *data)
 {
 	SCOPED_AO2LOCK(lock, app);
 
@@ -760,12 +760,12 @@ void app_update(struct app *app, stasis_app_cb handler, void *data)
 	app->data = data;
 }
 
-const char *app_name(const struct app *app)
+const char *app_name(const struct stasis_app *app)
 {
 	return app->name;
 }
 
-struct ast_json *app_to_json(const struct app *app)
+struct ast_json *app_to_json(const struct stasis_app *app)
 {
 	RAII_VAR(struct ast_json *, json, NULL, ast_json_unref);
 	struct ast_json *channels;
@@ -815,7 +815,7 @@ struct ast_json *app_to_json(const struct app *app)
 	return ast_json_ref(json);
 }
 
-int app_subscribe_channel(struct app *app, struct ast_channel *chan)
+int app_subscribe_channel(struct stasis_app *app, struct ast_channel *chan)
 {
 	int res;
 
@@ -846,7 +846,12 @@ int app_subscribe_channel(struct app *app, struct ast_channel *chan)
 	}
 }
 
-static int unsubscribe(struct app *app, const char *kind, const char *id)
+static int subscribe_channel(struct stasis_app *app, void *obj)
+{
+	return app_subscribe_channel(app, obj);
+}
+
+static int unsubscribe(struct stasis_app *app, const char *kind, const char *id)
 {
 	RAII_VAR(struct app_forwards *, forwards, NULL, ao2_cleanup);
 	SCOPED_AO2LOCK(lock, app->forwards);
@@ -870,7 +875,7 @@ static int unsubscribe(struct app *app, const char *kind, const char *id)
 	return 0;
 }
 
-int app_unsubscribe_channel(struct app *app, struct ast_channel *chan)
+int app_unsubscribe_channel(struct stasis_app *app, struct ast_channel *chan)
 {
 	if (!app || !chan) {
 		return -1;
@@ -879,7 +884,7 @@ int app_unsubscribe_channel(struct app *app, struct ast_channel *chan)
 	return app_unsubscribe_channel_id(app, ast_channel_uniqueid(chan));
 }
 
-int app_unsubscribe_channel_id(struct app *app, const char *channel_id)
+int app_unsubscribe_channel_id(struct stasis_app *app, const char *channel_id)
 {
 	if (!app || !channel_id) {
 		return -1;
@@ -888,14 +893,27 @@ int app_unsubscribe_channel_id(struct app *app, const char *channel_id)
 	return unsubscribe(app, "channel", channel_id);
 }
 
-int app_is_subscribed_channel_id(struct app *app, const char *channel_id)
+int app_is_subscribed_channel_id(struct stasis_app *app, const char *channel_id)
 {
 	RAII_VAR(struct app_forwards *, forwards, NULL, ao2_cleanup);
 	forwards = ao2_find(app->forwards, channel_id, OBJ_SEARCH_KEY);
 	return forwards != NULL;
 }
 
-int app_subscribe_bridge(struct app *app, struct ast_bridge *bridge)
+static void *channel_find(const struct stasis_app *app, const char *id)
+{
+	return ast_channel_get_by_name(id);
+}
+
+struct stasis_app_event_source channel_event_source = {
+	.scheme = "channel:",
+	.find = channel_find,
+	.subscribe = subscribe_channel,
+	.unsubscribe = app_unsubscribe_channel_id,
+	.is_subscribed = app_is_subscribed_channel_id
+};
+
+int app_subscribe_bridge(struct stasis_app *app, struct ast_bridge *bridge)
 {
 	if (!app || !bridge) {
 		return -1;
@@ -920,7 +938,12 @@ int app_subscribe_bridge(struct app *app, struct ast_bridge *bridge)
 	}
 }
 
-int app_unsubscribe_bridge(struct app *app, struct ast_bridge *bridge)
+static int subscribe_bridge(struct stasis_app *app, void *obj)
+{
+	return app_subscribe_bridge(app, obj);
+}
+
+int app_unsubscribe_bridge(struct stasis_app *app, struct ast_bridge *bridge)
 {
 	if (!app || !bridge) {
 		return -1;
@@ -929,7 +952,7 @@ int app_unsubscribe_bridge(struct app *app, struct ast_bridge *bridge)
 	return app_unsubscribe_bridge_id(app, bridge->uniqueid);
 }
 
-int app_unsubscribe_bridge_id(struct app *app, const char *bridge_id)
+int app_unsubscribe_bridge_id(struct stasis_app *app, const char *bridge_id)
 {
 	if (!app || !bridge_id) {
 		return -1;
@@ -938,14 +961,27 @@ int app_unsubscribe_bridge_id(struct app *app, const char *bridge_id)
 	return unsubscribe(app, "bridge", bridge_id);
 }
 
-int app_is_subscribed_bridge_id(struct app *app, const char *bridge_id)
+int app_is_subscribed_bridge_id(struct stasis_app *app, const char *bridge_id)
 {
 	RAII_VAR(struct app_forwards *, forwards, NULL, ao2_cleanup);
 	forwards = ao2_find(app->forwards, bridge_id, OBJ_SEARCH_KEY);
 	return forwards != NULL;
 }
 
-int app_subscribe_endpoint(struct app *app, struct ast_endpoint *endpoint)
+static void *bridge_find(const struct stasis_app *app, const char *id)
+{
+	return stasis_app_bridge_find_by_id(id);
+}
+
+struct stasis_app_event_source bridge_event_source = {
+	.scheme = "bridge:",
+	.find = bridge_find,
+	.subscribe = subscribe_bridge,
+	.unsubscribe = app_unsubscribe_bridge_id,
+	.is_subscribed = app_is_subscribed_bridge_id
+};
+
+int app_subscribe_endpoint(struct stasis_app *app, struct ast_endpoint *endpoint)
 {
 	if (!app || !endpoint) {
 		return -1;
@@ -970,7 +1006,12 @@ int app_subscribe_endpoint(struct app *app, struct ast_endpoint *endpoint)
 	}
 }
 
-int app_unsubscribe_endpoint_id(struct app *app, const char *endpoint_id)
+static int subscribe_endpoint(struct stasis_app *app, void *obj)
+{
+	return app_subscribe_endpoint(app, obj);
+}
+
+int app_unsubscribe_endpoint_id(struct stasis_app *app, const char *endpoint_id)
 {
 	if (!app || !endpoint_id) {
 		return -1;
@@ -979,9 +1020,45 @@ int app_unsubscribe_endpoint_id(struct app *app, const char *endpoint_id)
 	return unsubscribe(app, "endpoint", endpoint_id);
 }
 
-int app_is_subscribed_endpoint_id(struct app *app, const char *endpoint_id)
+int app_is_subscribed_endpoint_id(struct stasis_app *app, const char *endpoint_id)
 {
 	RAII_VAR(struct app_forwards *, forwards, NULL, ao2_cleanup);
 	forwards = ao2_find(app->forwards, endpoint_id, OBJ_SEARCH_KEY);
 	return forwards != NULL;
 }
+
+static void *endpoint_find(const struct stasis_app *app, const char *id)
+{
+	return ast_endpoint_find_by_id(id);
+}
+
+struct stasis_app_event_source endpoint_event_source = {
+	.scheme = "endpoint:",
+	.find = endpoint_find,
+	.subscribe = subscribe_endpoint,
+	.unsubscribe = app_unsubscribe_endpoint_id,
+	.is_subscribed = app_is_subscribed_endpoint_id
+};
+
+void stasis_app_register_event_sources(void)
+{
+	stasis_app_register_event_source(&channel_event_source);
+	stasis_app_register_event_source(&bridge_event_source);
+	stasis_app_register_event_source(&endpoint_event_source);
+}
+
+int stasis_app_is_core_event_source(struct stasis_app_event_source *obj)
+{
+	return obj == &endpoint_event_source ||
+		obj == &bridge_event_source ||
+		obj == &channel_event_source;
+}
+
+void stasis_app_unregister_event_sources(void)
+{
+	stasis_app_unregister_event_source(&endpoint_event_source);
+	stasis_app_unregister_event_source(&bridge_event_source);
+	stasis_app_unregister_event_source(&channel_event_source);
+}
+
+
diff --git a/res/stasis/app.h b/res/stasis/app.h
index 4db9db97d80d5763b7f4c1cd677227f789d9c50e..419ec54a85832bfb551c31a15fc2c46ca0e15891 100644
--- a/res/stasis/app.h
+++ b/res/stasis/app.h
@@ -34,7 +34,7 @@
 /*!
  * \brief Opaque pointer to \c res_stasis app structure.
  */
-struct app;
+struct stasis_app;
 
 /*!
  * \brief Create a res_stasis application.
@@ -45,7 +45,7 @@ struct app;
  * \return New \c res_stasis application.
  * \return \c NULL on error.
  */
-struct app *app_create(const char *name, stasis_app_cb handler, void *data);
+struct stasis_app *app_create(const char *name, stasis_app_cb handler, void *data);
 
 /*!
  * \brief Tears down an application.
@@ -54,7 +54,7 @@ struct app *app_create(const char *name, stasis_app_cb handler, void *data);
  *
  * \param app Application to unsubscribe.
  */
-void app_shutdown(struct app *app);
+void app_shutdown(struct stasis_app *app);
 
 /*!
  * \brief Deactivates an application.
@@ -64,7 +64,7 @@ void app_shutdown(struct app *app);
  *
  * \param app Application to deactivate.
  */
-void app_deactivate(struct app *app);
+void app_deactivate(struct stasis_app *app);
 
 /*!
  * \brief Checks whether an app is active.
@@ -73,7 +73,7 @@ void app_deactivate(struct app *app);
  * \return True (non-zero) if app is active.
  * \return False (zero) if app has been deactivated.
  */
-int app_is_active(struct app *app);
+int app_is_active(struct stasis_app *app);
 
 /*!
  * \brief Checks whether a deactivated app has no channels.
@@ -82,7 +82,7 @@ int app_is_active(struct app *app);
  * \param True (non-zero) if app is deactivated, and has no associated channels.
  * \param False (zero) otherwise.
  */
-int app_is_finished(struct app *app);
+int app_is_finished(struct stasis_app *app);
 
 /*!
  * \brief Update the handler and data for a \c res_stasis application.
@@ -93,7 +93,7 @@ int app_is_finished(struct app *app);
  * \param handler New application callback.
  * \param data New data pointer for the callback.
  */
-void app_update(struct app *app, stasis_app_cb handler, void *data);
+void app_update(struct stasis_app *app, stasis_app_cb handler, void *data);
 
 /*!
  * \brief Return an application's name.
@@ -102,7 +102,7 @@ void app_update(struct app *app, stasis_app_cb handler, void *data);
  * \return Name of the application.
  * \return \c NULL is \a app is \c NULL.
  */
-const char *app_name(const struct app *app);
+const char *app_name(const struct stasis_app *app);
 
 /*!
  * \brief Send a message to an application.
@@ -110,11 +110,11 @@ const char *app_name(const struct app *app);
  * \param app Application.
  * \param message Message to send.
  */
-void app_send(struct app *app, struct ast_json *message);
+void app_send(struct stasis_app *app, struct ast_json *message);
 
 struct app_forwards;
 
-struct ast_json *app_to_json(const struct app *app);
+struct ast_json *app_to_json(const struct stasis_app *app);
 
 /*!
  * \brief Subscribes an application to a channel.
@@ -124,7 +124,7 @@ struct ast_json *app_to_json(const struct app *app);
  * \return 0 on success.
  * \return Non-zero on error.
  */
-int app_subscribe_channel(struct app *app, struct ast_channel *chan);
+int app_subscribe_channel(struct stasis_app *app, struct ast_channel *chan);
 
 /*!
  * \brief Cancel the subscription an app has for a channel.
@@ -134,7 +134,7 @@ int app_subscribe_channel(struct app *app, struct ast_channel *chan);
  * \return 0 on success.
  * \return Non-zero on error.
  */
-int app_unsubscribe_channel(struct app *app, struct ast_channel *chan);
+int app_unsubscribe_channel(struct stasis_app *app, struct ast_channel *chan);
 
 /*!
  * \brief Cancel the subscription an app has for a channel.
@@ -144,7 +144,7 @@ int app_unsubscribe_channel(struct app *app, struct ast_channel *chan);
  * \return 0 on success.
  * \return Non-zero on error.
  */
-int app_unsubscribe_channel_id(struct app *app, const char *channel_id);
+int app_unsubscribe_channel_id(struct stasis_app *app, const char *channel_id);
 
 /*!
  * \brief Test if an app is subscribed to a channel.
@@ -154,7 +154,7 @@ int app_unsubscribe_channel_id(struct app *app, const char *channel_id);
  * \return True (non-zero) if channel is subscribed to \a app.
  * \return False (zero) if channel is not subscribed.
  */
-int app_is_subscribed_channel_id(struct app *app, const char *channel_id);
+int app_is_subscribed_channel_id(struct stasis_app *app, const char *channel_id);
 
 /*!
  * \brief Add a bridge subscription to an existing channel subscription.
@@ -164,7 +164,7 @@ int app_is_subscribed_channel_id(struct app *app, const char *channel_id);
  * \return 0 on success.
  * \return Non-zero on error.
  */
-int app_subscribe_bridge(struct app *app, struct ast_bridge *bridge);
+int app_subscribe_bridge(struct stasis_app *app, struct ast_bridge *bridge);
 
 /*!
  * \brief Cancel the bridge subscription for an application.
@@ -174,7 +174,7 @@ int app_subscribe_bridge(struct app *app, struct ast_bridge *bridge);
  * \return 0 on success.
  * \return Non-zero on error.
  */
-int app_unsubscribe_bridge(struct app *app, struct ast_bridge *bridge);
+int app_unsubscribe_bridge(struct stasis_app *app, struct ast_bridge *bridge);
 
 /*!
  * \brief Cancel the subscription an app has for a bridge.
@@ -184,7 +184,7 @@ int app_unsubscribe_bridge(struct app *app, struct ast_bridge *bridge);
  * \return 0 on success.
  * \return Non-zero on error.
  */
-int app_unsubscribe_bridge_id(struct app *app, const char *bridge_id);
+int app_unsubscribe_bridge_id(struct stasis_app *app, const char *bridge_id);
 
 /*!
  * \brief Test if an app is subscribed to a bridge.
@@ -194,7 +194,7 @@ int app_unsubscribe_bridge_id(struct app *app, const char *bridge_id);
  * \return True (non-zero) if bridge is subscribed to \a app.
  * \return False (zero) if bridge is not subscribed.
  */
-int app_is_subscribed_bridge_id(struct app *app, const char *bridge_id);
+int app_is_subscribed_bridge_id(struct stasis_app *app, const char *bridge_id);
 
 /*!
  * \brief Subscribes an application to a endpoint.
@@ -204,7 +204,7 @@ int app_is_subscribed_bridge_id(struct app *app, const char *bridge_id);
  * \return 0 on success.
  * \return Non-zero on error.
  */
-int app_subscribe_endpoint(struct app *app, struct ast_endpoint *endpoint);
+int app_subscribe_endpoint(struct stasis_app *app, struct ast_endpoint *endpoint);
 
 /*!
  * \brief Cancel the subscription an app has for a endpoint.
@@ -214,7 +214,7 @@ int app_subscribe_endpoint(struct app *app, struct ast_endpoint *endpoint);
  * \return 0 on success.
  * \return Non-zero on error.
  */
-int app_unsubscribe_endpoint_id(struct app *app, const char *endpoint_id);
+int app_unsubscribe_endpoint_id(struct stasis_app *app, const char *endpoint_id);
 
 /*!
  * \brief Test if an app is subscribed to a endpoint.
@@ -224,6 +224,6 @@ int app_unsubscribe_endpoint_id(struct app *app, const char *endpoint_id);
  * \return True (non-zero) if endpoint is subscribed to \a app.
  * \return False (zero) if endpoint is not subscribed.
  */
-int app_is_subscribed_endpoint_id(struct app *app, const char *endpoint_id);
+int app_is_subscribed_endpoint_id(struct stasis_app *app, const char *endpoint_id);
 
 #endif /* _ASTERISK_RES_STASIS_APP_H */
diff --git a/rest-api-templates/ari.make.mustache b/rest-api-templates/ari.make.mustache
index 37f09c17966695059251416d6cd33eed893e24bb..50293cf11a3b21deb60f29091a648f328df92fb8 100644
--- a/rest-api-templates/ari.make.mustache
+++ b/rest-api-templates/ari.make.mustache
@@ -19,8 +19,8 @@
 #
 
 {{#apis}}
-res_ari_{{name}}.so: ari/resource_{{name}}.o
+res_ari_{{c_name}}.so: ari/resource_{{c_name}}.o
 
-ari/resource_{{name}}.o: _ASTCFLAGS+=$(call MOD_ASTCFLAGS,res_ari_{{name}})
+ari/resource_{{c_name}}.o: _ASTCFLAGS+=$(call MOD_ASTCFLAGS,res_ari_{{c_name}})
 
 {{/apis}}
diff --git a/rest-api/api-docs/applications.json b/rest-api/api-docs/applications.json
index 494088dc51e43ef5aa6495d29d3e8628b439f9a2..cd27d586ec418f5b1ea2a17962f97397065070e9 100644
--- a/rest-api/api-docs/applications.json
+++ b/rest-api/api-docs/applications.json
@@ -68,7 +68,7 @@
 						},
 						{
 							"name": "eventSource",
-							"description": "URI for event source (channel:{channelId}, bridge:{bridgeId}, endpoint:{tech}/{resource}",
+							"description": "URI for event source (channel:{channelId}, bridge:{bridgeId}, endpoint:{tech}/{resource}, deviceState:{deviceName}",
 							"paramType": "query",
 							"required": true,
 							"allowMultiple": true,
@@ -107,7 +107,7 @@
 						},
 						{
 							"name": "eventSource",
-							"description": "URI for event source (channel:{channelId}, bridge:{bridgeId}, endpoint:{tech}/{resource}",
+							"description": "URI for event source (channel:{channelId}, bridge:{bridgeId}, endpoint:{tech}/{resource}, device_state:{deviceName}",
 							"paramType": "query",
 							"required": true,
 							"allowMultiple": true,
@@ -160,6 +160,11 @@
 					"type": "List[string]",
 					"description": "{tech}/{resource} for endpoints subscribed to.",
 					"required": true
+				},
+				"device_names": {
+					"type": "List[string]",
+					"description": "Names of the devices subscribed to.",
+					"required": true
 				}
 			}
 		}
diff --git a/rest-api/api-docs/deviceStates.json b/rest-api/api-docs/deviceStates.json
new file mode 100644
index 0000000000000000000000000000000000000000..529bc70da8e539877b46dc916a3b3e23b07470a3
--- /dev/null
+++ b/rest-api/api-docs/deviceStates.json
@@ -0,0 +1,151 @@
+{
+	"_copyright": "Copyright (C) 2012 - 2013, Digium, Inc.",
+	"_author": "Kevin Harwell <kharwell@digium.com>",
+	"_svn_revision": "$Revision$",
+	"apiVersion": "0.0.1",
+	"swaggerVersion": "1.1",
+	"basePath": "http://localhost:8088/stasis",
+	"resourcePath": "/api-docs/deviceStates.{format}",
+	"apis": [
+		{
+			"path": "/deviceStates",
+			"description": "Device states",
+			"operations": [
+				{
+					"httpMethod": "GET",
+					"summary": "List all ARI controlled device states.",
+					"nickname": "list",
+					"responseClass": "List[DeviceState]"
+				}
+			]
+		},
+		{
+			"path": "/deviceStates/{deviceName}",
+			"description": "Device state",
+			"operations": [
+				{
+					"httpMethod": "GET",
+					"summary": "Retrieve the current state of a device.",
+					"nickname": "get",
+					"responseClass": "DeviceState",
+					"parameters": [
+						{
+							"name": "deviceName",
+							"description": "Name of the device",
+							"paramType": "path",
+							"required": true,
+							"allowMultiple": false,
+							"dataType": "string"
+						}
+					]
+				},
+				{
+					"httpMethod": "PUT",
+					"summary": "Change the state of a device controlled by ARI. (Note - implicitly creates the device state).",
+					"nickname": "update",
+					"responseClass": "void",
+					"parameters": [
+						{
+							"name": "deviceName",
+							"description": "Name of the device",
+							"paramType": "path",
+							"required": true,
+							"allowMultiple": false,
+							"dataType": "string"
+						},
+						{
+							"name": "deviceState",
+							"description": "Device state value",
+							"paramType": "query",
+							"required": true,
+							"allowMultiple": false,
+							"dataType": "string",
+						        "allowableValues": {
+							         "valueType": "LIST",
+							          "values": [
+								          "NOT_INUSE",
+								          "INUSE",
+								          "BUSY",
+								          "INVALID",
+								          "UNAVAILABLE",
+								          "RINGING",
+								          "RINGINUSE",
+								          "ONHOLD"
+								  ]
+						    }
+
+						}
+					],
+				        "errorResponses": [
+						{
+							"code": 404,
+							"reason": "Device name is missing"
+						},
+						{
+							"code": 409,
+							"reason": "Uncontrolled device specified"
+						}
+					]
+				},
+				{
+					"httpMethod": "DELETE",
+					"summary": "Destroy a device-state controlled by ARI.",
+					"nickname": "delete",
+					"responseClass": "void",
+					"parameters": [
+						{
+							"name": "deviceName",
+							"description": "Name of the device",
+							"paramType": "path",
+							"required": true,
+							"allowMultiple": false,
+							"dataType": "string"
+						}
+					],
+				        "errorResponses": [
+						{
+							"code": 404,
+							"reason": "Device name is missing"
+						},
+						{
+							"code": 409,
+							"reason": "Uncontrolled device specified"
+						}
+					]
+				}
+			]
+		}
+	],
+	"models": {
+		"DeviceState": {
+			"id": "DeviceState",
+			"description": "Represents the state of a device.",
+			"properties": {
+				"name": {
+					"type": "string",
+					"description": "Name of the device.",
+					"required": true
+				},
+				"state": {
+					"type": "string",
+					"description": "Device's state",
+					"required": true,
+					"allowableValues": {
+						"valueType": "LIST",
+						"values": [
+							"UNKNOWN",
+							"NOT_INUSE",
+							"INUSE",
+							"BUSY",
+							"INVALID",
+							"UNAVAILABLE",
+							"RINGING",
+							"RINGINUSE",
+							"ONHOLD"
+						]
+					}
+				}
+			}
+		}
+	}
+}
diff --git a/rest-api/api-docs/events.json b/rest-api/api-docs/events.json
index e30a193c1132bf4d8092bff08da09228224aa844..a342099a81fdd41a1f8845ce006ac4efeba095a6 100644
--- a/rest-api/api-docs/events.json
+++ b/rest-api/api-docs/events.json
@@ -76,6 +76,7 @@
 				}
 			},
 			"subTypes": [
+				"DeviceStateChanged",
 				"PlaybackStarted",
 				"PlaybackFinished",
 				"ApplicationReplaced",
@@ -98,6 +99,17 @@
 				"StasisStart"
 			]
 		},
+		"DeviceStateChanged": {
+			"id": "DeviceStateChanged",
+			"description": "Notification that a device state has changed.",
+			"properties": {
+				"device_state": {
+					"type": "DeviceState",
+					"description": "Device state object",
+					"required": true
+				}
+			}
+		},
 		"PlaybackStarted": {
 			"id": "PlaybackStarted",
 			"description": "Event showing the start of a media playback operation.",
diff --git a/rest-api/resources.json b/rest-api/resources.json
index 7e0b793fbb689276eacefcdbe58cc92bb2eac69a..495348a21b53d0ef4d532ce947a2e14dcfe71738 100644
--- a/rest-api/resources.json
+++ b/rest-api/resources.json
@@ -34,6 +34,10 @@
 			"path": "/api-docs/playbacks.{format}",
 			"description": "Playback control resources"
 		},
+		{
+			"path": "/api-docs/deviceStates.{format}",
+			"description": "Device state resources"
+		},
 		{
 			"path": "/api-docs/events.{format}",
 			"description": "WebSocket resource"