diff --git a/Makefile b/Makefile
index a51534424296bd5d2f145728aede045e6b570527..d896c3e360c4864981b0643250fcc1a4955cad17 100644
--- a/Makefile
+++ b/Makefile
@@ -536,7 +536,8 @@ OLDHEADERS=$(filter-out $(NEWHEADERS) $(notdir $(DESTDIR)$(ASTHEADERDIR)),$(notd
 INSTALLDIRS="$(ASTLIBDIR)" "$(ASTMODDIR)" "$(ASTSBINDIR)" "$(ASTETCDIR)" "$(ASTVARRUNDIR)" \
 	"$(ASTSPOOLDIR)" "$(ASTSPOOLDIR)/dictate" "$(ASTSPOOLDIR)/meetme" \
 	"$(ASTSPOOLDIR)/monitor" "$(ASTSPOOLDIR)/system" "$(ASTSPOOLDIR)/tmp" \
-	"$(ASTSPOOLDIR)/voicemail" "$(ASTHEADERDIR)" "$(ASTHEADERDIR)/doxygen" \
+	"$(ASTSPOOLDIR)/voicemail" "$(ASTSPOOLDIR)/recording" \
+	"$(ASTHEADERDIR)" "$(ASTHEADERDIR)/doxygen" \
 	"$(ASTLOGDIR)" "$(ASTLOGDIR)/cdr-csv" "$(ASTLOGDIR)/cdr-custom" \
 	"$(ASTLOGDIR)/cel-custom" "$(ASTDATADIR)" "$(ASTDATADIR)/documentation" \
 	"$(ASTDATADIR)/documentation/thirdparty" "$(ASTDATADIR)/firmware" \
diff --git a/apps/app_minivm.c b/apps/app_minivm.c
index ba6d6e5a292bb080c6d59815d27b9be47df0aa6a..2b6f7e4b87842c9e393fade53ff360aa691759e2 100644
--- a/apps/app_minivm.c
+++ b/apps/app_minivm.c
@@ -1674,7 +1674,7 @@ static int play_record_review(struct ast_channel *chan, char *playfile, char *re
 				ast_channel_setoption(chan, AST_OPTION_RXGAIN, &record_gain, sizeof(record_gain), 0);
 			if (ast_test_flag(vmu, MVM_OPERATOR))
 				canceldtmf = "0";
-			cmd = ast_play_and_record_full(chan, playfile, recordfile, maxtime, fmt, duration, sound_duration, global_silencethreshold, global_maxsilence, unlockdir, acceptdtmf, canceldtmf);
+			cmd = ast_play_and_record_full(chan, playfile, recordfile, maxtime, fmt, duration, sound_duration, global_silencethreshold, global_maxsilence, unlockdir, acceptdtmf, canceldtmf, 0, AST_RECORD_IF_EXISTS_OVERWRITE);
 			if (record_gain)
 				ast_channel_setoption(chan, AST_OPTION_RXGAIN, &zero_gain, sizeof(zero_gain), 0);
 			if (cmd == -1) /* User has hung up, no options to give */
diff --git a/apps/app_voicemail.c b/apps/app_voicemail.c
index 90458bb31dd7234d00f2015d27c50b6e9766ead8..95265b5b1010f5dd74f5a917bbcdb839ea5df4a8 100644
--- a/apps/app_voicemail.c
+++ b/apps/app_voicemail.c
@@ -14684,7 +14684,7 @@ static int play_record_review(struct ast_channel *chan, char *playfile, char *re
 				ast_channel_setoption(chan, AST_OPTION_RXGAIN, &record_gain, sizeof(record_gain), 0);
 			if (ast_test_flag(vmu, VM_OPERATOR))
 				canceldtmf = "0";
-			cmd = ast_play_and_record_full(chan, playfile, tempfile, maxtime, fmt, duration, sound_duration, silencethreshold, maxsilence, unlockdir, acceptdtmf, canceldtmf);
+			cmd = ast_play_and_record_full(chan, playfile, tempfile, maxtime, fmt, duration, sound_duration, silencethreshold, maxsilence, unlockdir, acceptdtmf, canceldtmf, 0, AST_RECORD_IF_EXISTS_OVERWRITE);
 			if (strchr(canceldtmf, cmd)) {
 			/* need this flag here to distinguish between pressing '0' during message recording or after */
 				canceleddtmf = 1;
diff --git a/include/asterisk/app.h b/include/asterisk/app.h
index 7ddacfc4e9068515b704554062f1909f0ccb76f0..91438a2d0fdfcbe6168676a0c6156b94346f327e 100644
--- a/include/asterisk/app.h
+++ b/include/asterisk/app.h
@@ -690,9 +690,23 @@ int ast_control_streamfile_w_cb(struct ast_channel *chan,
 /*! \brief Play a stream and wait for a digit, returning the digit that was pressed */
 int ast_play_and_wait(struct ast_channel *chan, const char *fn);
 
+/*!
+ * Possible actions to take if a recording already exists
+ * \since 12
+ */
+enum ast_record_if_exists {
+	/*! Fail the recording. */
+	AST_RECORD_IF_EXISTS_FAIL,
+	/*! Overwrite the existing recording. */
+	AST_RECORD_IF_EXISTS_OVERWRITE,
+	/*! Append to the existing recording. */
+	AST_RECORD_IF_EXISTS_APPEND,
+};
+
 /*!
  * \brief Record a file based on input from a channel
- *        This function will play "auth-thankyou" upon successful recording.
+ *        This function will play "auth-thankyou" upon successful recording if
+ *        skip_confirmation_sound is false.
  *
  * \param chan the channel being recorded
  * \param playfile Filename of sound to play before recording begins
@@ -706,13 +720,15 @@ int ast_play_and_wait(struct ast_channel *chan, const char *fn);
  * \param path Optional filesystem path to unlock
  * \param acceptdtmf Character of DTMF to end and accept the recording
  * \param canceldtmf Character of DTMF to end and cancel the recording
+ * \param skip_confirmation_sound If true, don't play auth-thankyou at end. Nice for custom recording prompts in apps.
+ * \param if_exists Action to take if recording already exists.
  *
  * \retval -1 failure or hangup
  * \retval 'S' Recording ended from silence timeout
  * \retval 't' Recording ended from the message exceeding the maximum duration
  * \retval dtmfchar Recording ended via the return value's DTMF character for either cancel or accept.
  */
-int ast_play_and_record_full(struct ast_channel *chan, const char *playfile, const char *recordfile, int maxtime_sec, const char *fmt, int *duration, int *sound_duration, int silencethreshold, int maxsilence_ms, const char *path, const char *acceptdtmf, const char *canceldtmf);
+int ast_play_and_record_full(struct ast_channel *chan, const char *playfile, const char *recordfile, int maxtime_sec, const char *fmt, int *duration, int *sound_duration, int silencethreshold, int maxsilence_ms, const char *path, const char *acceptdtmf, const char *canceldtmf, int skip_confirmation_sound, enum ast_record_if_exists if_exists);
 
 /*!
  * \brief Record a file based on input from a channel. Use default accept and cancel DTMF.
diff --git a/include/asterisk/channel.h b/include/asterisk/channel.h
index fae43d423bc1d6f9c4422a93a1cedae5c4e5c43b..d61494141d549c39a70722b798a77f153d2bd42a 100644
--- a/include/asterisk/channel.h
+++ b/include/asterisk/channel.h
@@ -1603,6 +1603,18 @@ void ast_channel_setwhentohangup_tv(struct ast_channel *chan, struct timeval off
  */
 int ast_answer(struct ast_channel *chan);
 
+/*!
+ * \brief Answer a channel, if it's not already answered.
+ *
+ * \param chan channel to answer
+ *
+ * \details See ast_answer()
+ *
+ * \retval 0 on success
+ * \retval non-zero on failure
+ */
+int ast_auto_answer(struct ast_channel *chan);
+
 /*!
  * \brief Answer a channel
  *
diff --git a/include/asterisk/file.h b/include/asterisk/file.h
index 844b434293edd7c0f1fc52c15ec96bd21d42ee0a..372c0f7ed26a66f600e508a830a8333848fad788 100644
--- a/include/asterisk/file.h
+++ b/include/asterisk/file.h
@@ -64,8 +64,8 @@ enum ast_waitstream_fr_cb_values {
  */
 typedef void (ast_waitstream_fr_cb)(struct ast_channel *chan, long ms, enum ast_waitstream_fr_cb_values val);
 
-/*! 
- * \brief Streams a file 
+/*!
+ * \brief Streams a file
  * \param c channel to stream the file to
  * \param filename the name of the file you wish to stream, minus the extension
  * \param preflang the preferred language you wish to have the file streamed to you in
@@ -86,12 +86,12 @@ int ast_streamfile(struct ast_channel *c, const char *filename, const char *pref
  */
 int ast_stream_and_wait(struct ast_channel *chan, const char *file, const char *digits);
 
-/*! 
- * \brief Stops a stream 
+/*!
+ * \brief Stops a stream
  *
  * \param c The channel you wish to stop playback on
  *
- * Stop playback of a stream 
+ * Stop playback of a stream
  *
  * \retval 0 always
  *
diff --git a/include/asterisk/paths.h b/include/asterisk/paths.h
index 14da7aaf993aa2242cf623b7e80cb29317f3aa67..ea0c561237174b87daff6d44f6c5e9a0061d2141 100644
--- a/include/asterisk/paths.h
+++ b/include/asterisk/paths.h
@@ -23,6 +23,7 @@ extern const char *ast_config_AST_CONFIG_FILE;
 extern const char *ast_config_AST_MODULE_DIR;
 extern const char *ast_config_AST_SPOOL_DIR;
 extern const char *ast_config_AST_MONITOR_DIR;
+extern const char *ast_config_AST_RECORDING_DIR;
 extern const char *ast_config_AST_VAR_DIR;
 extern const char *ast_config_AST_DATA_DIR;
 extern const char *ast_config_AST_LOG_DIR;
diff --git a/include/asterisk/stasis_app_recording.h b/include/asterisk/stasis_app_recording.h
new file mode 100644
index 0000000000000000000000000000000000000000..9c9930406b9653e61e08e1714fc6dd5dbee5a7e6
--- /dev/null
+++ b/include/asterisk/stasis_app_recording.h
@@ -0,0 +1,203 @@
+/*
+ * Asterisk -- An open source telephony toolkit.
+ *
+ * Copyright (C) 2013, Digium, Inc.
+ *
+ * David M. Lee, II <dlee@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_RECORDING_H
+#define _ASTERISK_STASIS_APP_RECORDING_H
+
+/*! \file
+ *
+ * \brief Stasis Application Recording API. See \ref res_stasis "Stasis
+ * Application API" for detailed documentation.
+ *
+ * \author David M. Lee, II <dlee@digium.com>
+ * \since 12
+ */
+
+#include "asterisk/app.h"
+#include "asterisk/stasis_app.h"
+
+/*! Opaque struct for handling the recording of media to a file. */
+struct stasis_app_recording;
+
+/*! State of a recording operation */
+enum stasis_app_recording_state {
+	/*! The recording has not started yet */
+	STASIS_APP_RECORDING_STATE_QUEUED,
+	/*! The media is currently recording */
+	STASIS_APP_RECORDING_STATE_RECORDING,
+	/*! The media is currently paused */
+	STASIS_APP_RECORDING_STATE_PAUSED,
+	/*! The media has stopped recording */
+	STASIS_APP_RECORDING_STATE_COMPLETE,
+	/*! The media has stopped playing */
+	STASIS_APP_RECORDING_STATE_FAILED,
+};
+
+/*! Valid operation for controlling a recording. */
+enum stasis_app_recording_media_operation {
+	/*! Stop the recording operation. */
+	STASIS_APP_RECORDING_STOP,
+};
+
+#define STASIS_APP_RECORDING_TERMINATE_INVALID 0
+#define STASIS_APP_RECORDING_TERMINATE_NONE -1
+#define STASIS_APP_RECORDING_TERMINATE_ANY -2
+
+struct stasis_app_recording_options {
+	AST_DECLARE_STRING_FIELDS(
+		AST_STRING_FIELD(name);	/*!< name Name of the recording. */
+		AST_STRING_FIELD(format);	/*!< Format to be recorded (wav, gsm, etc.) */
+		);
+	/*! Number of seconds of silence before ending the recording. */
+	int max_silence_seconds;
+	/*! Maximum recording duration. 0 for no maximum. */
+	int max_duration_seconds;
+	/*! Which DTMF to use to terminate the recording
+	 *  \c STASIS_APP_RECORDING_TERMINATE_NONE to terminate only on hangup
+	 *  \c STASIS_APP_RECORDING_TERMINATE_ANY to terminate on any DTMF
+	 */
+	char terminate_on;
+	/*! How to handle recording when a file already exists */
+	enum ast_record_if_exists if_exists;
+	/*! If true, a beep is played at the start of recording */
+	int beep:1;
+};
+
+/*!
+ * \brief Allocate a recording options object.
+ *
+ * Clean up with ao2_cleanup().
+ *
+ * \param name Name of the recording.
+ * \param format Format to record in.
+ * \return Newly allocated options object.
+ * \return \c NULL on error.
+ */
+struct stasis_app_recording_options *stasis_app_recording_options_create(
+	const char *name, const char *format);
+
+/*!
+ * \brief Parse a string into the recording termination enum.
+ *
+ * \param str String to parse.
+ * \return DTMF value to terminate on.
+ * \return \c STASIS_APP_RECORDING_TERMINATE_NONE to not terminate on DTMF.
+ * \return \c STASIS_APP_RECORDING_TERMINATE_ANY to terminate on any DTMF.
+ * \return \c STASIS_APP_RECORDING_TERMINATE_INVALID if input was invalid.
+ */
+char stasis_app_recording_termination_parse(const char *str);
+
+/*!
+ * \brief Parse a string into the if_exists enum.
+ *
+ * \param str String to parse.
+ * \return How to handle an existing file.
+ * \return -1 on error.
+ */
+enum ast_record_if_exists stasis_app_recording_if_exists_parse(
+	const char *str);
+
+/*!
+ * \brief Record media from a channel.
+ *
+ * A reference to the \a options object may be kept, so it MUST NOT be modified
+ * after calling this function.
+ *
+ * On error, \c errno is set to indicate the failure reason.
+ *  - \c EINVAL: Invalid input.
+ *  - \c EEXIST: A recording with that name is in session.
+ *  - \c ENOMEM: Out of memory.
+ *
+ * \param control Control for \c res_stasis.
+ * \param options Recording options.
+ * \return Recording control object.
+ * \return \c NULL on error.
+ */
+struct stasis_app_recording *stasis_app_control_record(
+	struct stasis_app_control *control,
+	struct stasis_app_recording_options *options);
+
+/*!
+ * \brief Gets the current state of a recording operation.
+ *
+ * \param recording Recording control object.
+ * \return The state of the \a recording object.
+ */
+enum stasis_app_recording_state stasis_app_recording_get_state(
+	struct stasis_app_recording *recording);
+
+/*!
+ * \brief Gets the unique name of a recording object.
+ *
+ * \param recording Recording control object.
+ * \return \a recording's name.
+ * \return \c NULL if \a recording ic \c NULL
+ */
+const char *stasis_app_recording_get_name(
+	struct stasis_app_recording *recording);
+
+/*!
+ * \brief Finds the recording object with the given name.
+ *
+ * \param name Name of the recording object to find.
+ * \return Associated \ref stasis_app_recording object.
+ * \return \c NULL if \a name not found.
+ */
+struct stasis_app_recording *stasis_app_recording_find_by_name(const char *name);
+
+/*!
+ * \brief Construct a JSON model of a recording.
+ *
+ * \param recording Recording to conver.
+ * \return JSON model.
+ * \return \c NULL on error.
+ */
+struct ast_json *stasis_app_recording_to_json(
+	const struct stasis_app_recording *recording);
+
+/*!
+ * \brief Possible results from a recording operation.
+ */
+enum stasis_app_recording_oper_results {
+	/*! Operation completed successfully. */
+	STASIS_APP_RECORDING_OPER_OK,
+	/*! Operation failed. */
+	STASIS_APP_RECORDING_OPER_FAILED,
+	/*! Operation failed b/c recording is not in session. */
+	STASIS_APP_RECORDING_OPER_NOT_RECORDING,
+};
+
+/*!
+ * \brief Controls the media for a given recording operation.
+ *
+ * \param recording Recording control object.
+ * \param control Media control operation.
+ * \return \c STASIS_APP_RECORDING_OPER_OK on success.
+ * \return \ref stasis_app_recording_oper_results indicating failure.
+ */
+enum stasis_app_recording_oper_results stasis_app_recording_operation(
+	struct stasis_app_recording *recording,
+	enum stasis_app_recording_media_operation operation);
+
+/*!
+ * \brief Message type for recording updates. The data is an
+ * \ref ast_channel_blob.
+ */
+struct stasis_message_type *stasis_app_recording_snapshot_type(void);
+
+#endif /* _ASTERISK_STASIS_APP_RECORDING_H */
diff --git a/include/asterisk/utils.h b/include/asterisk/utils.h
index ce6db0965bfd9940993126a74927f89bbd797323..1848509053c80593d8650f098ca1a0307fec383e 100644
--- a/include/asterisk/utils.h
+++ b/include/asterisk/utils.h
@@ -718,6 +718,19 @@ void ast_enable_packet_fragmentation(int sock);
  */
 int ast_mkdir(const char *path, int mode);
 
+/*!
+ * \brief Recursively create directory path, but only if it resolves within
+ * the given \a base_path.
+ *
+ * If \a base_path does not exist, it will not be created and this function
+ * returns \c EPERM.
+ *
+ * \param path The directory path to create
+ * \param mode The permissions with which to try to create the directory
+ * \return 0 on success or an error code otherwise
+ */
+int ast_safe_mkdir(const char *base_path, const char *path, int mode);
+
 #define ARRAY_LEN(a) (size_t) (sizeof(a) / sizeof(0[a]))
 
 
diff --git a/main/app.c b/main/app.c
index a7a9029c95dd65c4f3f3090d7b7527bb251c6173..031f6f28f593190d2e5ef3ec8b676a106f1980a7 100644
--- a/main/app.c
+++ b/main/app.c
@@ -1169,7 +1169,7 @@ static int global_maxsilence = 0;
  * \retval 't' Recording ended from the message exceeding the maximum duration, or via DTMF in prepend mode
  * \retval dtmfchar Recording ended via the return value's DTMF character for either cancel or accept.
  */
-static int __ast_play_and_record(struct ast_channel *chan, const char *playfile, const char *recordfile, int maxtime, const char *fmt, int *duration, int *sound_duration, int beep, int silencethreshold, int maxsilence, const char *path, int prepend, const char *acceptdtmf, const char *canceldtmf, int skip_confirmation_sound)
+static int __ast_play_and_record(struct ast_channel *chan, const char *playfile, const char *recordfile, int maxtime, const char *fmt, int *duration, int *sound_duration, int beep, int silencethreshold, int maxsilence, const char *path, int prepend, const char *acceptdtmf, const char *canceldtmf, int skip_confirmation_sound, enum ast_record_if_exists if_exists)
 {
 	int d = 0;
 	char *fmts;
@@ -1186,6 +1186,21 @@ static int __ast_play_and_record(struct ast_channel *chan, const char *playfile,
 	struct ast_format rfmt;
 	struct ast_silence_generator *silgen = NULL;
 	char prependfile[PATH_MAX];
+	int ioflags;	/* IO flags for writing output file */
+
+	ioflags = O_CREAT|O_WRONLY;
+
+	switch (if_exists) {
+	case AST_RECORD_IF_EXISTS_FAIL:
+		ioflags |= O_EXCL;
+		break;
+	case AST_RECORD_IF_EXISTS_OVERWRITE:
+		ioflags |= O_TRUNC;
+		break;
+	case AST_RECORD_IF_EXISTS_APPEND:
+		ioflags |= O_APPEND;
+		break;
+	}
 
 	ast_format_clear(&rfmt);
 	if (silencethreshold < 0) {
@@ -1239,7 +1254,7 @@ static int __ast_play_and_record(struct ast_channel *chan, const char *playfile,
 
 	end = start = time(NULL);  /* pre-initialize end to be same as start in case we never get into loop */
 	for (x = 0; x < fmtcnt; x++) {
-		others[x] = ast_writefile(prepend ? prependfile : recordfile, sfmt[x], comment, O_TRUNC, 0, AST_FILE_MODE);
+		others[x] = ast_writefile(prepend ? prependfile : recordfile, sfmt[x], comment, ioflags, 0, AST_FILE_MODE);
 		ast_verb(3, "x=%d, open writing:  %s format: %s, %p\n", x, prepend ? prependfile : recordfile, sfmt[x], others[x]);
 
 		if (!others[x]) {
@@ -1477,19 +1492,19 @@ static int __ast_play_and_record(struct ast_channel *chan, const char *playfile,
 static const char default_acceptdtmf[] = "#";
 static const char default_canceldtmf[] = "";
 
-int ast_play_and_record_full(struct ast_channel *chan, const char *playfile, const char *recordfile, int maxtime, const char *fmt, int *duration, int *sound_duration, int silencethreshold, int maxsilence, const char *path, const char *acceptdtmf, const char *canceldtmf)
+int ast_play_and_record_full(struct ast_channel *chan, const char *playfile, const char *recordfile, int maxtime, const char *fmt, int *duration, int *sound_duration, int silencethreshold, int maxsilence, const char *path, const char *acceptdtmf, const char *canceldtmf, int skip_confirmation_sound, enum ast_record_if_exists if_exists)
 {
-	return __ast_play_and_record(chan, playfile, recordfile, maxtime, fmt, duration, sound_duration, 0, silencethreshold, maxsilence, path, 0, S_OR(acceptdtmf, default_acceptdtmf), S_OR(canceldtmf, default_canceldtmf), 0);
+	return __ast_play_and_record(chan, playfile, recordfile, maxtime, fmt, duration, sound_duration, 0, silencethreshold, maxsilence, path, 0, S_OR(acceptdtmf, default_acceptdtmf), S_OR(canceldtmf, default_canceldtmf), skip_confirmation_sound, if_exists);
 }
 
 int ast_play_and_record(struct ast_channel *chan, const char *playfile, const char *recordfile, int maxtime, const char *fmt, int *duration, int *sound_duration, int silencethreshold, int maxsilence, const char *path)
 {
-	return __ast_play_and_record(chan, playfile, recordfile, maxtime, fmt, duration, sound_duration, 0, silencethreshold, maxsilence, path, 0, default_acceptdtmf, default_canceldtmf, 0);
+	return __ast_play_and_record(chan, playfile, recordfile, maxtime, fmt, duration, sound_duration, 0, silencethreshold, maxsilence, path, 0, default_acceptdtmf, default_canceldtmf, 0, AST_RECORD_IF_EXISTS_OVERWRITE);
 }
 
 int ast_play_and_prepend(struct ast_channel *chan, char *playfile, char *recordfile, int maxtime, char *fmt, int *duration, int *sound_duration, int beep, int silencethreshold, int maxsilence)
 {
-	return __ast_play_and_record(chan, playfile, recordfile, maxtime, fmt, duration, sound_duration, beep, silencethreshold, maxsilence, NULL, 1, default_acceptdtmf, default_canceldtmf, 1);
+	return __ast_play_and_record(chan, playfile, recordfile, maxtime, fmt, duration, sound_duration, beep, silencethreshold, maxsilence, NULL, 1, default_acceptdtmf, default_canceldtmf, 1, AST_RECORD_IF_EXISTS_OVERWRITE);
 }
 
 /* Channel group core functions */
diff --git a/main/asterisk.c b/main/asterisk.c
index c1ce2c0f187a32bfd070a7b744c525bcce49f9f8..aa33f31e4834c14b0fd0e6a07b3b0e0ff24c7161 100644
--- a/main/asterisk.c
+++ b/main/asterisk.c
@@ -373,6 +373,7 @@ struct _cfg_paths {
 	char module_dir[PATH_MAX];
 	char spool_dir[PATH_MAX];
 	char monitor_dir[PATH_MAX];
+	char recording_dir[PATH_MAX];
 	char var_dir[PATH_MAX];
 	char data_dir[PATH_MAX];
 	char log_dir[PATH_MAX];
@@ -397,6 +398,7 @@ const char *ast_config_AST_CONFIG_FILE	= cfg_paths.config_file;
 const char *ast_config_AST_MODULE_DIR	= cfg_paths.module_dir;
 const char *ast_config_AST_SPOOL_DIR	= cfg_paths.spool_dir;
 const char *ast_config_AST_MONITOR_DIR	= cfg_paths.monitor_dir;
+const char *ast_config_AST_RECORDING_DIR	= cfg_paths.recording_dir;
 const char *ast_config_AST_VAR_DIR	= cfg_paths.var_dir;
 const char *ast_config_AST_DATA_DIR	= cfg_paths.data_dir;
 const char *ast_config_AST_LOG_DIR	= cfg_paths.log_dir;
@@ -3306,6 +3308,7 @@ static void ast_readconfig(void)
 	ast_copy_string(cfg_paths.spool_dir, DEFAULT_SPOOL_DIR, sizeof(cfg_paths.spool_dir));
 	ast_copy_string(cfg_paths.module_dir, DEFAULT_MODULE_DIR, sizeof(cfg_paths.module_dir));
 	snprintf(cfg_paths.monitor_dir, sizeof(cfg_paths.monitor_dir), "%s/monitor", cfg_paths.spool_dir);
+	snprintf(cfg_paths.recording_dir, sizeof(cfg_paths.recording_dir), "%s/recording", cfg_paths.spool_dir);
 	ast_copy_string(cfg_paths.var_dir, DEFAULT_VAR_DIR, sizeof(cfg_paths.var_dir));
 	ast_copy_string(cfg_paths.data_dir, DEFAULT_DATA_DIR, sizeof(cfg_paths.data_dir));
 	ast_copy_string(cfg_paths.log_dir, DEFAULT_LOG_DIR, sizeof(cfg_paths.log_dir));
@@ -3341,6 +3344,7 @@ static void ast_readconfig(void)
 		} else if (!strcasecmp(v->name, "astspooldir")) {
 			ast_copy_string(cfg_paths.spool_dir, v->value, sizeof(cfg_paths.spool_dir));
 			snprintf(cfg_paths.monitor_dir, sizeof(cfg_paths.monitor_dir), "%s/monitor", v->value);
+			snprintf(cfg_paths.recording_dir, sizeof(cfg_paths.recording_dir), "%s/recording", v->value);
 		} else if (!strcasecmp(v->name, "astvarlibdir")) {
 			ast_copy_string(cfg_paths.var_dir, v->value, sizeof(cfg_paths.var_dir));
 			if (!found.dbdir)
diff --git a/main/channel.c b/main/channel.c
index 9d1ec69c27218afb263135947daa5230560732f7..3bc5c0a75c9e02f4de1086f90ed0e164b8494d07 100644
--- a/main/channel.c
+++ b/main/channel.c
@@ -3029,6 +3029,15 @@ int ast_answer(struct ast_channel *chan)
 	return __ast_answer(chan, 0);
 }
 
+inline int ast_auto_answer(struct ast_channel *chan)
+{
+	if (ast_channel_state(chan) == AST_STATE_UP) {
+		/* Already answered */
+		return 0;
+	}
+	return ast_answer(chan);
+}
+
 int ast_channel_get_duration(struct ast_channel *chan)
 {
 	ast_assert(NULL != chan);
diff --git a/main/file.c b/main/file.c
index 016afd197d0e9ffbf14a28056bc79b0f18b26ef8..cb495b3100d27fff87fc26146912ac3768967805 100644
--- a/main/file.c
+++ b/main/file.c
@@ -1020,6 +1020,9 @@ int ast_closestream(struct ast_filestream *f)
 	 * We close the stream in order to quit queuing frames now, because we might
 	 * change the writeformat, which could result in a subsequent write error, if
 	 * the format is different. */
+	if (f == NULL) {
+		return 0;
+	}
 	filestream_close(f);
 	ao2_ref(f, -1);
 	return 0;
diff --git a/main/utils.c b/main/utils.c
index 208a4d32614d01f0bab6fdbe808ed8cb3c94ceb6..04f6127033fe04d9ca6ff0c53429c2994716795b 100644
--- a/main/utils.c
+++ b/main/utils.c
@@ -2105,6 +2105,100 @@ int ast_mkdir(const char *path, int mode)
 	return 0;
 }
 
+static int safe_mkdir(const char *base_path, char *path, int mode)
+{
+	RAII_VAR(char *, absolute_path, NULL, free);
+
+	absolute_path = realpath(path, NULL);
+
+	if (absolute_path) {
+		/* Path exists, but is it in the right place? */
+		if (!ast_begins_with(absolute_path, base_path)) {
+			return EPERM;
+		}
+
+		/* It is in the right place! */
+		return 0;
+	} else {
+		/* Path doesn't exist. */
+
+		/* The slash terminating the subpath we're checking */
+		char *path_term = strchr(path, '/');
+		/* True indicates the parent path is within base_path */
+		int parent_is_safe = 0;
+		int res;
+
+		while (path_term) {
+			RAII_VAR(char *, absolute_subpath, NULL, free);
+
+			/* Truncate the path one past the slash */
+			char c = *(path_term + 1);
+			*(path_term + 1) = '\0';
+			absolute_subpath = realpath(path, NULL);
+
+			if (absolute_subpath) {
+				/* Subpath exists, but is it safe? */
+				parent_is_safe = ast_begins_with(
+					absolute_subpath, base_path);
+			} else if (parent_is_safe) {
+				/* Subpath does not exist, but parent is safe
+				 * Create it */
+				res = mkdir(path, mode);
+				if (res != 0) {
+					ast_assert(errno != EEXIST);
+					return errno;
+				}
+			} else {
+				/* Subpath did not exist, parent was not safe
+				 * Fail! */
+				errno = EPERM;
+				return errno;
+			}
+			/* Restore the path */
+			*(path_term + 1) = c;
+			/* Move on to the next slash */
+			path_term = strchr(path_term + 1, '/');
+		}
+
+		/* Now to build the final path, but only if it's safe */
+		if (!parent_is_safe) {
+			errno = EPERM;
+			return errno;
+		}
+
+		res = mkdir(path, mode);
+		if (res != 0 && errno != EEXIST) {
+			return errno;
+		}
+
+		return 0;
+	}
+}
+
+int ast_safe_mkdir(const char *base_path, const char *path, int mode)
+{
+	RAII_VAR(char *, absolute_base_path, NULL, free);
+	RAII_VAR(char *, p, NULL, ast_free);
+
+	if (base_path == NULL || path == NULL) {
+		errno = EFAULT;
+		return errno;
+	}
+
+	p = ast_strdup(path);
+	if (p == NULL) {
+		errno = ENOMEM;
+		return errno;
+	}
+
+	absolute_base_path = realpath(base_path, NULL);
+	if (absolute_base_path == NULL) {
+		return errno;
+	}
+
+	return safe_mkdir(absolute_base_path, p, mode);
+}
+
 int ast_utils_init(void)
 {
 	dev_urandom_fd = open("/dev/urandom", O_RDONLY);
diff --git a/res/res_stasis_http_bridges.c b/res/res_stasis_http_bridges.c
index a4801df13dd97ff35b9ba0d1967a6afa5cfe85a0..878c1ce0ae7c83a1cf3baaa827df63c5b1534c46 100644
--- a/res/res_stasis_http_bridges.c
+++ b/res/res_stasis_http_bridges.c
@@ -387,10 +387,10 @@ static void stasis_http_record_bridge_cb(
 			args.max_silence_seconds = atoi(i->value);
 		} else
 		if (strcmp(i->name, "append") == 0) {
-			args.append = atoi(i->value);
+			args.append = ast_true(i->value);
 		} else
 		if (strcmp(i->name, "beep") == 0) {
-			args.beep = atoi(i->value);
+			args.beep = ast_true(i->value);
 		} else
 		if (strcmp(i->name, "terminateOn") == 0) {
 			args.terminate_on = (i->value);
diff --git a/res/res_stasis_http_channels.c b/res/res_stasis_http_channels.c
index ebcc9e8800e8cae1bfccfe39d547197b44acd82f..5343714b1af5dbf597dd256ff310a642891cf009 100644
--- a/res/res_stasis_http_channels.c
+++ b/res/res_stasis_http_channels.c
@@ -765,11 +765,11 @@ static void stasis_http_record_channel_cb(
 		if (strcmp(i->name, "maxSilenceSeconds") == 0) {
 			args.max_silence_seconds = atoi(i->value);
 		} else
-		if (strcmp(i->name, "append") == 0) {
-			args.append = atoi(i->value);
+		if (strcmp(i->name, "ifExists") == 0) {
+			args.if_exists = (i->value);
 		} else
 		if (strcmp(i->name, "beep") == 0) {
-			args.beep = atoi(i->value);
+			args.beep = ast_true(i->value);
 		} else
 		if (strcmp(i->name, "terminateOn") == 0) {
 			args.terminate_on = (i->value);
@@ -788,8 +788,9 @@ static void stasis_http_record_channel_cb(
 
 	switch (code) {
 	case 500: /* Internal server error */
+	case 400: /* Invalid parameters */
 	case 404: /* Channel not found */
-	case 409: /* Channel is not in a Stasis application, or the channel is currently bridged with other channels. */
+	case 409: /* Channel is not in a Stasis application; the channel is currently bridged with other channels; A recording with the same name is currently in progress. */
 		is_valid = 1;
 		break;
 	default:
diff --git a/res/res_stasis_http_recordings.c b/res/res_stasis_http_recordings.c
index 4aa43c9be3b8e47d695d4940ca4819e34b7bd4c8..5b8043251333efe8c790413321ec7b997c4edc17 100644
--- a/res/res_stasis_http_recordings.c
+++ b/res/res_stasis_http_recordings.c
@@ -91,7 +91,7 @@ static void stasis_http_get_stored_recordings_cb(
 #endif /* AST_DEVMODE */
 }
 /*!
- * \brief Parameter parsing callback for /recordings/stored/{recordingId}.
+ * \brief Parameter parsing callback for /recordings/stored/{recordingName}.
  * \param get_params GET parameters in the HTTP request.
  * \param path_vars Path variables extracted from the request.
  * \param headers HTTP headers.
@@ -110,8 +110,8 @@ static void stasis_http_get_stored_recording_cb(
 	struct ast_variable *i;
 
 	for (i = path_vars; i; i = i->next) {
-		if (strcmp(i->name, "recordingId") == 0) {
-			args.recording_id = (i->value);
+		if (strcmp(i->name, "recordingName") == 0) {
+			args.recording_name = (i->value);
 		} else
 		{}
 	}
@@ -128,20 +128,20 @@ static void stasis_http_get_stored_recording_cb(
 			is_valid = ari_validate_stored_recording(
 				response->message);
 		} else {
-			ast_log(LOG_ERROR, "Invalid error response %d for /recordings/stored/{recordingId}\n", code);
+			ast_log(LOG_ERROR, "Invalid error response %d for /recordings/stored/{recordingName}\n", code);
 			is_valid = 0;
 		}
 	}
 
 	if (!is_valid) {
-		ast_log(LOG_ERROR, "Response validation failed for /recordings/stored/{recordingId}\n");
+		ast_log(LOG_ERROR, "Response validation failed for /recordings/stored/{recordingName}\n");
 		stasis_http_response_error(response, 500,
 			"Internal Server Error", "Response validation failed");
 	}
 #endif /* AST_DEVMODE */
 }
 /*!
- * \brief Parameter parsing callback for /recordings/stored/{recordingId}.
+ * \brief Parameter parsing callback for /recordings/stored/{recordingName}.
  * \param get_params GET parameters in the HTTP request.
  * \param path_vars Path variables extracted from the request.
  * \param headers HTTP headers.
@@ -160,8 +160,8 @@ static void stasis_http_delete_stored_recording_cb(
 	struct ast_variable *i;
 
 	for (i = path_vars; i; i = i->next) {
-		if (strcmp(i->name, "recordingId") == 0) {
-			args.recording_id = (i->value);
+		if (strcmp(i->name, "recordingName") == 0) {
+			args.recording_name = (i->value);
 		} else
 		{}
 	}
@@ -178,13 +178,13 @@ static void stasis_http_delete_stored_recording_cb(
 			is_valid = ari_validate_void(
 				response->message);
 		} else {
-			ast_log(LOG_ERROR, "Invalid error response %d for /recordings/stored/{recordingId}\n", code);
+			ast_log(LOG_ERROR, "Invalid error response %d for /recordings/stored/{recordingName}\n", code);
 			is_valid = 0;
 		}
 	}
 
 	if (!is_valid) {
-		ast_log(LOG_ERROR, "Response validation failed for /recordings/stored/{recordingId}\n");
+		ast_log(LOG_ERROR, "Response validation failed for /recordings/stored/{recordingName}\n");
 		stasis_http_response_error(response, 500,
 			"Internal Server Error", "Response validation failed");
 	}
@@ -233,7 +233,7 @@ static void stasis_http_get_live_recordings_cb(
 #endif /* AST_DEVMODE */
 }
 /*!
- * \brief Parameter parsing callback for /recordings/live/{recordingId}.
+ * \brief Parameter parsing callback for /recordings/live/{recordingName}.
  * \param get_params GET parameters in the HTTP request.
  * \param path_vars Path variables extracted from the request.
  * \param headers HTTP headers.
@@ -252,8 +252,8 @@ static void stasis_http_get_live_recording_cb(
 	struct ast_variable *i;
 
 	for (i = path_vars; i; i = i->next) {
-		if (strcmp(i->name, "recordingId") == 0) {
-			args.recording_id = (i->value);
+		if (strcmp(i->name, "recordingName") == 0) {
+			args.recording_name = (i->value);
 		} else
 		{}
 	}
@@ -270,20 +270,20 @@ static void stasis_http_get_live_recording_cb(
 			is_valid = ari_validate_live_recording(
 				response->message);
 		} else {
-			ast_log(LOG_ERROR, "Invalid error response %d for /recordings/live/{recordingId}\n", code);
+			ast_log(LOG_ERROR, "Invalid error response %d for /recordings/live/{recordingName}\n", code);
 			is_valid = 0;
 		}
 	}
 
 	if (!is_valid) {
-		ast_log(LOG_ERROR, "Response validation failed for /recordings/live/{recordingId}\n");
+		ast_log(LOG_ERROR, "Response validation failed for /recordings/live/{recordingName}\n");
 		stasis_http_response_error(response, 500,
 			"Internal Server Error", "Response validation failed");
 	}
 #endif /* AST_DEVMODE */
 }
 /*!
- * \brief Parameter parsing callback for /recordings/live/{recordingId}.
+ * \brief Parameter parsing callback for /recordings/live/{recordingName}.
  * \param get_params GET parameters in the HTTP request.
  * \param path_vars Path variables extracted from the request.
  * \param headers HTTP headers.
@@ -302,8 +302,8 @@ static void stasis_http_cancel_recording_cb(
 	struct ast_variable *i;
 
 	for (i = path_vars; i; i = i->next) {
-		if (strcmp(i->name, "recordingId") == 0) {
-			args.recording_id = (i->value);
+		if (strcmp(i->name, "recordingName") == 0) {
+			args.recording_name = (i->value);
 		} else
 		{}
 	}
@@ -320,20 +320,20 @@ static void stasis_http_cancel_recording_cb(
 			is_valid = ari_validate_void(
 				response->message);
 		} else {
-			ast_log(LOG_ERROR, "Invalid error response %d for /recordings/live/{recordingId}\n", code);
+			ast_log(LOG_ERROR, "Invalid error response %d for /recordings/live/{recordingName}\n", code);
 			is_valid = 0;
 		}
 	}
 
 	if (!is_valid) {
-		ast_log(LOG_ERROR, "Response validation failed for /recordings/live/{recordingId}\n");
+		ast_log(LOG_ERROR, "Response validation failed for /recordings/live/{recordingName}\n");
 		stasis_http_response_error(response, 500,
 			"Internal Server Error", "Response validation failed");
 	}
 #endif /* AST_DEVMODE */
 }
 /*!
- * \brief Parameter parsing callback for /recordings/live/{recordingId}/stop.
+ * \brief Parameter parsing callback for /recordings/live/{recordingName}/stop.
  * \param get_params GET parameters in the HTTP request.
  * \param path_vars Path variables extracted from the request.
  * \param headers HTTP headers.
@@ -352,8 +352,8 @@ static void stasis_http_stop_recording_cb(
 	struct ast_variable *i;
 
 	for (i = path_vars; i; i = i->next) {
-		if (strcmp(i->name, "recordingId") == 0) {
-			args.recording_id = (i->value);
+		if (strcmp(i->name, "recordingName") == 0) {
+			args.recording_name = (i->value);
 		} else
 		{}
 	}
@@ -370,20 +370,20 @@ static void stasis_http_stop_recording_cb(
 			is_valid = ari_validate_void(
 				response->message);
 		} else {
-			ast_log(LOG_ERROR, "Invalid error response %d for /recordings/live/{recordingId}/stop\n", code);
+			ast_log(LOG_ERROR, "Invalid error response %d for /recordings/live/{recordingName}/stop\n", code);
 			is_valid = 0;
 		}
 	}
 
 	if (!is_valid) {
-		ast_log(LOG_ERROR, "Response validation failed for /recordings/live/{recordingId}/stop\n");
+		ast_log(LOG_ERROR, "Response validation failed for /recordings/live/{recordingName}/stop\n");
 		stasis_http_response_error(response, 500,
 			"Internal Server Error", "Response validation failed");
 	}
 #endif /* AST_DEVMODE */
 }
 /*!
- * \brief Parameter parsing callback for /recordings/live/{recordingId}/pause.
+ * \brief Parameter parsing callback for /recordings/live/{recordingName}/pause.
  * \param get_params GET parameters in the HTTP request.
  * \param path_vars Path variables extracted from the request.
  * \param headers HTTP headers.
@@ -402,8 +402,8 @@ static void stasis_http_pause_recording_cb(
 	struct ast_variable *i;
 
 	for (i = path_vars; i; i = i->next) {
-		if (strcmp(i->name, "recordingId") == 0) {
-			args.recording_id = (i->value);
+		if (strcmp(i->name, "recordingName") == 0) {
+			args.recording_name = (i->value);
 		} else
 		{}
 	}
@@ -420,20 +420,20 @@ static void stasis_http_pause_recording_cb(
 			is_valid = ari_validate_void(
 				response->message);
 		} else {
-			ast_log(LOG_ERROR, "Invalid error response %d for /recordings/live/{recordingId}/pause\n", code);
+			ast_log(LOG_ERROR, "Invalid error response %d for /recordings/live/{recordingName}/pause\n", code);
 			is_valid = 0;
 		}
 	}
 
 	if (!is_valid) {
-		ast_log(LOG_ERROR, "Response validation failed for /recordings/live/{recordingId}/pause\n");
+		ast_log(LOG_ERROR, "Response validation failed for /recordings/live/{recordingName}/pause\n");
 		stasis_http_response_error(response, 500,
 			"Internal Server Error", "Response validation failed");
 	}
 #endif /* AST_DEVMODE */
 }
 /*!
- * \brief Parameter parsing callback for /recordings/live/{recordingId}/unpause.
+ * \brief Parameter parsing callback for /recordings/live/{recordingName}/unpause.
  * \param get_params GET parameters in the HTTP request.
  * \param path_vars Path variables extracted from the request.
  * \param headers HTTP headers.
@@ -452,8 +452,8 @@ static void stasis_http_unpause_recording_cb(
 	struct ast_variable *i;
 
 	for (i = path_vars; i; i = i->next) {
-		if (strcmp(i->name, "recordingId") == 0) {
-			args.recording_id = (i->value);
+		if (strcmp(i->name, "recordingName") == 0) {
+			args.recording_name = (i->value);
 		} else
 		{}
 	}
@@ -470,20 +470,20 @@ static void stasis_http_unpause_recording_cb(
 			is_valid = ari_validate_void(
 				response->message);
 		} else {
-			ast_log(LOG_ERROR, "Invalid error response %d for /recordings/live/{recordingId}/unpause\n", code);
+			ast_log(LOG_ERROR, "Invalid error response %d for /recordings/live/{recordingName}/unpause\n", code);
 			is_valid = 0;
 		}
 	}
 
 	if (!is_valid) {
-		ast_log(LOG_ERROR, "Response validation failed for /recordings/live/{recordingId}/unpause\n");
+		ast_log(LOG_ERROR, "Response validation failed for /recordings/live/{recordingName}/unpause\n");
 		stasis_http_response_error(response, 500,
 			"Internal Server Error", "Response validation failed");
 	}
 #endif /* AST_DEVMODE */
 }
 /*!
- * \brief Parameter parsing callback for /recordings/live/{recordingId}/mute.
+ * \brief Parameter parsing callback for /recordings/live/{recordingName}/mute.
  * \param get_params GET parameters in the HTTP request.
  * \param path_vars Path variables extracted from the request.
  * \param headers HTTP headers.
@@ -502,8 +502,8 @@ static void stasis_http_mute_recording_cb(
 	struct ast_variable *i;
 
 	for (i = path_vars; i; i = i->next) {
-		if (strcmp(i->name, "recordingId") == 0) {
-			args.recording_id = (i->value);
+		if (strcmp(i->name, "recordingName") == 0) {
+			args.recording_name = (i->value);
 		} else
 		{}
 	}
@@ -520,20 +520,20 @@ static void stasis_http_mute_recording_cb(
 			is_valid = ari_validate_void(
 				response->message);
 		} else {
-			ast_log(LOG_ERROR, "Invalid error response %d for /recordings/live/{recordingId}/mute\n", code);
+			ast_log(LOG_ERROR, "Invalid error response %d for /recordings/live/{recordingName}/mute\n", code);
 			is_valid = 0;
 		}
 	}
 
 	if (!is_valid) {
-		ast_log(LOG_ERROR, "Response validation failed for /recordings/live/{recordingId}/mute\n");
+		ast_log(LOG_ERROR, "Response validation failed for /recordings/live/{recordingName}/mute\n");
 		stasis_http_response_error(response, 500,
 			"Internal Server Error", "Response validation failed");
 	}
 #endif /* AST_DEVMODE */
 }
 /*!
- * \brief Parameter parsing callback for /recordings/live/{recordingId}/unmute.
+ * \brief Parameter parsing callback for /recordings/live/{recordingName}/unmute.
  * \param get_params GET parameters in the HTTP request.
  * \param path_vars Path variables extracted from the request.
  * \param headers HTTP headers.
@@ -552,8 +552,8 @@ static void stasis_http_unmute_recording_cb(
 	struct ast_variable *i;
 
 	for (i = path_vars; i; i = i->next) {
-		if (strcmp(i->name, "recordingId") == 0) {
-			args.recording_id = (i->value);
+		if (strcmp(i->name, "recordingName") == 0) {
+			args.recording_name = (i->value);
 		} else
 		{}
 	}
@@ -570,13 +570,13 @@ static void stasis_http_unmute_recording_cb(
 			is_valid = ari_validate_void(
 				response->message);
 		} else {
-			ast_log(LOG_ERROR, "Invalid error response %d for /recordings/live/{recordingId}/unmute\n", code);
+			ast_log(LOG_ERROR, "Invalid error response %d for /recordings/live/{recordingName}/unmute\n", code);
 			is_valid = 0;
 		}
 	}
 
 	if (!is_valid) {
-		ast_log(LOG_ERROR, "Response validation failed for /recordings/live/{recordingId}/unmute\n");
+		ast_log(LOG_ERROR, "Response validation failed for /recordings/live/{recordingName}/unmute\n");
 		stasis_http_response_error(response, 500,
 			"Internal Server Error", "Response validation failed");
 	}
@@ -584,8 +584,8 @@ static void stasis_http_unmute_recording_cb(
 }
 
 /*! \brief REST handler for /api-docs/recordings.{format} */
-static struct stasis_rest_handlers recordings_stored_recordingId = {
-	.path_segment = "recordingId",
+static struct stasis_rest_handlers recordings_stored_recordingName = {
+	.path_segment = "recordingName",
 	.is_wildcard = 1,
 	.callbacks = {
 		[AST_HTTP_GET] = stasis_http_get_stored_recording_cb,
@@ -601,10 +601,10 @@ static struct stasis_rest_handlers recordings_stored = {
 		[AST_HTTP_GET] = stasis_http_get_stored_recordings_cb,
 	},
 	.num_children = 1,
-	.children = { &recordings_stored_recordingId, }
+	.children = { &recordings_stored_recordingName, }
 };
 /*! \brief REST handler for /api-docs/recordings.{format} */
-static struct stasis_rest_handlers recordings_live_recordingId_stop = {
+static struct stasis_rest_handlers recordings_live_recordingName_stop = {
 	.path_segment = "stop",
 	.callbacks = {
 		[AST_HTTP_POST] = stasis_http_stop_recording_cb,
@@ -613,7 +613,7 @@ static struct stasis_rest_handlers recordings_live_recordingId_stop = {
 	.children = {  }
 };
 /*! \brief REST handler for /api-docs/recordings.{format} */
-static struct stasis_rest_handlers recordings_live_recordingId_pause = {
+static struct stasis_rest_handlers recordings_live_recordingName_pause = {
 	.path_segment = "pause",
 	.callbacks = {
 		[AST_HTTP_POST] = stasis_http_pause_recording_cb,
@@ -622,7 +622,7 @@ static struct stasis_rest_handlers recordings_live_recordingId_pause = {
 	.children = {  }
 };
 /*! \brief REST handler for /api-docs/recordings.{format} */
-static struct stasis_rest_handlers recordings_live_recordingId_unpause = {
+static struct stasis_rest_handlers recordings_live_recordingName_unpause = {
 	.path_segment = "unpause",
 	.callbacks = {
 		[AST_HTTP_POST] = stasis_http_unpause_recording_cb,
@@ -631,7 +631,7 @@ static struct stasis_rest_handlers recordings_live_recordingId_unpause = {
 	.children = {  }
 };
 /*! \brief REST handler for /api-docs/recordings.{format} */
-static struct stasis_rest_handlers recordings_live_recordingId_mute = {
+static struct stasis_rest_handlers recordings_live_recordingName_mute = {
 	.path_segment = "mute",
 	.callbacks = {
 		[AST_HTTP_POST] = stasis_http_mute_recording_cb,
@@ -640,7 +640,7 @@ static struct stasis_rest_handlers recordings_live_recordingId_mute = {
 	.children = {  }
 };
 /*! \brief REST handler for /api-docs/recordings.{format} */
-static struct stasis_rest_handlers recordings_live_recordingId_unmute = {
+static struct stasis_rest_handlers recordings_live_recordingName_unmute = {
 	.path_segment = "unmute",
 	.callbacks = {
 		[AST_HTTP_POST] = stasis_http_unmute_recording_cb,
@@ -649,15 +649,15 @@ static struct stasis_rest_handlers recordings_live_recordingId_unmute = {
 	.children = {  }
 };
 /*! \brief REST handler for /api-docs/recordings.{format} */
-static struct stasis_rest_handlers recordings_live_recordingId = {
-	.path_segment = "recordingId",
+static struct stasis_rest_handlers recordings_live_recordingName = {
+	.path_segment = "recordingName",
 	.is_wildcard = 1,
 	.callbacks = {
 		[AST_HTTP_GET] = stasis_http_get_live_recording_cb,
 		[AST_HTTP_DELETE] = stasis_http_cancel_recording_cb,
 	},
 	.num_children = 5,
-	.children = { &recordings_live_recordingId_stop,&recordings_live_recordingId_pause,&recordings_live_recordingId_unpause,&recordings_live_recordingId_mute,&recordings_live_recordingId_unmute, }
+	.children = { &recordings_live_recordingName_stop,&recordings_live_recordingName_pause,&recordings_live_recordingName_unpause,&recordings_live_recordingName_mute,&recordings_live_recordingName_unmute, }
 };
 /*! \brief REST handler for /api-docs/recordings.{format} */
 static struct stasis_rest_handlers recordings_live = {
@@ -666,7 +666,7 @@ static struct stasis_rest_handlers recordings_live = {
 		[AST_HTTP_GET] = stasis_http_get_live_recordings_cb,
 	},
 	.num_children = 1,
-	.children = { &recordings_live_recordingId, }
+	.children = { &recordings_live_recordingName, }
 };
 /*! \brief REST handler for /api-docs/recordings.{format} */
 static struct stasis_rest_handlers recordings = {
diff --git a/res/res_stasis_playback.c b/res/res_stasis_playback.c
index 3b092df2dfe91a62070763f07429ac00dbf0b98f..5b55ebc51eec6827f769a0ed491ce82032648387 100644
--- a/res/res_stasis_playback.c
+++ b/res/res_stasis_playback.c
@@ -37,6 +37,7 @@ ASTERISK_FILE_VERSION(__FILE__, "$Revision$")
 #include "asterisk/file.h"
 #include "asterisk/logger.h"
 #include "asterisk/module.h"
+#include "asterisk/paths.h"
 #include "asterisk/stasis_app_impl.h"
 #include "asterisk/stasis_app_playback.h"
 #include "asterisk/stasis_channels.h"
@@ -195,7 +196,7 @@ static void *play_uri(struct stasis_app_control *control,
 	RAII_VAR(struct stasis_app_playback *, playback, NULL,
 		playback_cleanup);
 	RAII_VAR(struct ast_json *, json, NULL, ast_json_unref);
-	const char *file;
+	RAII_VAR(char *, file, NULL, ast_free);
 	int res;
 	long offsetms;
 
@@ -225,16 +226,27 @@ static void *play_uri(struct stasis_app_control *control,
 
 	if (ast_begins_with(playback->media, SOUND_URI_SCHEME)) {
 		/* Play sound */
-		file = playback->media + strlen(SOUND_URI_SCHEME);
+		file = ast_strdup(playback->media + strlen(SOUND_URI_SCHEME));
 	} else if (ast_begins_with(playback->media, RECORDING_URI_SCHEME)) {
 		/* Play recording */
-		file = playback->media + strlen(RECORDING_URI_SCHEME);
+		const char *relname =
+			playback->media + strlen(RECORDING_URI_SCHEME);
+		if (relname[0] == '/') {
+			file = ast_strdup(relname);
+		} else {
+			ast_asprintf(&file, "%s/%s",
+				ast_config_AST_RECORDING_DIR, relname);
+		}
 	} else {
 		/* Play URL */
 		ast_log(LOG_ERROR, "Unimplemented\n");
 		return NULL;
 	}
 
+	if (!file) {
+		return NULL;
+	}
+
 	res = ast_control_streamfile_lang(chan, file, fwd, rev, stop, pause,
 		restart, playback->skipms, playback->language, &offsetms);
 
diff --git a/res/res_stasis_recording.c b/res/res_stasis_recording.c
new file mode 100644
index 0000000000000000000000000000000000000000..3d8e11bbd7aa3c8015856bdc355e2af011b07285
--- /dev/null
+++ b/res/res_stasis_recording.c
@@ -0,0 +1,443 @@
+/*
+ * Asterisk -- An open source telephony toolkit.
+ *
+ * Copyright (C) 2013, Digium, Inc.
+ *
+ * David M. Lee, II <dlee@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 res_stasis recording support.
+ *
+ * \author David M. Lee, II <dlee@digium.com>
+ */
+
+/*** MODULEINFO
+	<depend type="module">res_stasis</depend>
+	<support_level>core</support_level>
+ ***/
+
+#include "asterisk.h"
+
+ASTERISK_FILE_VERSION(__FILE__, "$Revision$")
+
+#include "asterisk/dsp.h"
+#include "asterisk/file.h"
+#include "asterisk/module.h"
+#include "asterisk/paths.h"
+#include "asterisk/stasis_app_impl.h"
+#include "asterisk/stasis_app_recording.h"
+#include "asterisk/stasis_channels.h"
+
+/*! Number of hash buckets for recording container. Keep it prime! */
+#define RECORDING_BUCKETS 127
+
+/*! Comment is ignored by most formats, so we will ignore it, too. */
+#define RECORDING_COMMENT NULL
+
+/*! Recording check is unimplemented. le sigh */
+#define RECORDING_CHECK 0
+
+STASIS_MESSAGE_TYPE_DEFN(stasis_app_recording_snapshot_type);
+
+/*! Container of all current recordings */
+static struct ao2_container *recordings;
+
+struct stasis_app_recording {
+	/*! Recording options. */
+	struct stasis_app_recording_options *options;
+	/*! Absolute path (minus extension) of the recording */
+	char *absolute_name;
+	/*! Control object for the channel we're playing back to */
+	struct stasis_app_control *control;
+
+	/*! Current state of the recording. */
+	enum stasis_app_recording_state state;
+};
+
+static int recording_hash(const void *obj, int flags)
+{
+	const struct stasis_app_recording *recording = obj;
+	const char *id = flags & OBJ_KEY ? obj : recording->options->name;
+	return ast_str_hash(id);
+}
+
+static int recording_cmp(void *obj, void *arg, int flags)
+{
+	struct stasis_app_recording *lhs = obj;
+	struct stasis_app_recording *rhs = arg;
+	const char *rhs_id = flags & OBJ_KEY ? arg : rhs->options->name;
+
+	if (strcmp(lhs->options->name, rhs_id) == 0) {
+		return CMP_MATCH | CMP_STOP;
+	} else {
+		return 0;
+	}
+}
+
+static const char *state_to_string(enum stasis_app_recording_state state)
+{
+	switch (state) {
+	case STASIS_APP_RECORDING_STATE_QUEUED:
+		return "queued";
+	case STASIS_APP_RECORDING_STATE_RECORDING:
+		return "recording";
+	case STASIS_APP_RECORDING_STATE_PAUSED:
+		return "paused";
+	case STASIS_APP_RECORDING_STATE_COMPLETE:
+		return "done";
+	case STASIS_APP_RECORDING_STATE_FAILED:
+		return "failed";
+	}
+
+	return "?";
+}
+
+static void recording_options_dtor(void *obj)
+{
+	struct stasis_app_recording_options *options = obj;
+
+	ast_string_field_free_memory(options);
+}
+
+struct stasis_app_recording_options *stasis_app_recording_options_create(
+	const char *name, const char *format)
+{
+	RAII_VAR(struct stasis_app_recording_options *, options, NULL,
+		ao2_cleanup);
+
+	options = ao2_alloc(sizeof(*options), recording_options_dtor);
+
+	if (!options || ast_string_field_init(options, 128)) {
+		return NULL;
+	}
+	ast_string_field_set(options, name, name);
+	ast_string_field_set(options, format, format);
+
+	ao2_ref(options, +1);
+	return options;
+}
+
+char stasis_app_recording_termination_parse(const char *str)
+{
+	if (ast_strlen_zero(str)) {
+		return STASIS_APP_RECORDING_TERMINATE_NONE;
+	}
+
+	if (strcasecmp(str, "none") == 0) {
+		return STASIS_APP_RECORDING_TERMINATE_NONE;
+	}
+
+	if (strcasecmp(str, "any") == 0) {
+		return STASIS_APP_RECORDING_TERMINATE_ANY;
+	}
+
+	if (strcasecmp(str, "#") == 0) {
+		return '#';
+	}
+
+	if (strcasecmp(str, "*") == 0) {
+		return '*';
+	}
+
+	return STASIS_APP_RECORDING_TERMINATE_INVALID;
+}
+
+enum ast_record_if_exists stasis_app_recording_if_exists_parse(
+	const char *str)
+{
+	if (ast_strlen_zero(str)) {
+		/* Default value */
+		return AST_RECORD_IF_EXISTS_FAIL;
+	}
+
+	if (strcasecmp(str, "fail") == 0) {
+		return AST_RECORD_IF_EXISTS_FAIL;
+	}
+
+	if (strcasecmp(str, "overwrite") == 0) {
+		return AST_RECORD_IF_EXISTS_OVERWRITE;
+	}
+
+	if (strcasecmp(str, "append") == 0) {
+		return AST_RECORD_IF_EXISTS_APPEND;
+	}
+
+	return -1;
+}
+
+static void recording_publish(struct stasis_app_recording *recording)
+{
+	RAII_VAR(struct ast_json *, json, NULL, ast_json_unref);
+	RAII_VAR(struct ast_channel_snapshot *, snapshot, NULL, ao2_cleanup);
+	RAII_VAR(struct stasis_message *, message, NULL, ao2_cleanup);
+
+	ast_assert(recording != NULL);
+
+	json = stasis_app_recording_to_json(recording);
+	if (json == NULL) {
+		return;
+	}
+
+	message = ast_channel_blob_create_from_cache(
+		stasis_app_control_get_channel_id(recording->control),
+		stasis_app_recording_snapshot_type(), json);
+	if (message == NULL) {
+		return;
+	}
+
+	stasis_app_control_publish(recording->control, message);
+}
+
+static void recording_fail(struct stasis_app_recording *recording)
+{
+	SCOPED_AO2LOCK(lock, recording);
+	recording->state = STASIS_APP_RECORDING_STATE_FAILED;
+	recording_publish(recording);
+}
+
+static void recording_cleanup(struct stasis_app_recording *recording)
+{
+	ao2_unlink_flags(recordings, recording,
+		OBJ_POINTER | OBJ_UNLINK | OBJ_NODATA);
+}
+
+static void *record_file(struct stasis_app_control *control,
+	struct ast_channel *chan, void *data)
+{
+	RAII_VAR(struct stasis_app_recording *, recording,
+		NULL, recording_cleanup);
+	char *acceptdtmf;
+	int res;
+	int duration = 0;
+
+	recording = data;
+	ast_assert(recording != NULL);
+
+	ao2_lock(recording);
+	recording->state = STASIS_APP_RECORDING_STATE_RECORDING;
+	recording_publish(recording);
+	ao2_unlock(recording);
+
+	switch (recording->options->terminate_on) {
+	case STASIS_APP_RECORDING_TERMINATE_NONE:
+	case STASIS_APP_RECORDING_TERMINATE_INVALID:
+		acceptdtmf = "";
+		break;
+	case STASIS_APP_RECORDING_TERMINATE_ANY:
+		acceptdtmf = "#*0123456789abcd";
+		break;
+	default:
+		acceptdtmf = ast_alloca(2);
+		acceptdtmf[0] = recording->options->terminate_on;
+		acceptdtmf[1] = '\0';
+	}
+
+	res = ast_auto_answer(chan);
+	if (res != 0) {
+		ast_debug(3, "%s: Failed to answer\n",
+			ast_channel_uniqueid(chan));
+		recording_fail(recording);
+		return NULL;
+	}
+
+	ast_play_and_record_full(chan,
+		recording->options->beep ? "beep" : NULL,
+		recording->absolute_name,
+		recording->options->max_duration_seconds,
+		recording->options->format,
+		&duration,
+		NULL, /* sound_duration */
+		-1, /* silencethreshold */
+		recording->options->max_silence_seconds * 1000,
+		NULL, /* path */
+		acceptdtmf,
+		NULL, /* canceldtmf */
+		1, /* skip_confirmation_sound */
+		recording->options->if_exists);
+
+	ast_debug(3, "%s: Recording complete\n", ast_channel_uniqueid(chan));
+
+	ao2_lock(recording);
+	recording->state = STASIS_APP_RECORDING_STATE_COMPLETE;
+	recording_publish(recording);
+	ao2_unlock(recording);
+
+	return NULL;
+}
+
+static void recording_dtor(void *obj)
+{
+	struct stasis_app_recording *recording = obj;
+
+	ao2_cleanup(recording->options);
+}
+
+struct stasis_app_recording *stasis_app_control_record(
+	struct stasis_app_control *control,
+	struct stasis_app_recording_options *options)
+{
+	RAII_VAR(struct stasis_app_recording *, recording, NULL, ao2_cleanup);
+	char *last_slash;
+
+	errno = 0;
+
+	if (options == NULL ||
+		ast_strlen_zero(options->name) ||
+		ast_strlen_zero(options->format) ||
+		options->max_silence_seconds < 0 ||
+		options->max_duration_seconds < 0) {
+		errno = EINVAL;
+		return NULL;
+	}
+
+	ast_debug(3, "%s: Sending record(%s.%s) command\n",
+		stasis_app_control_get_channel_id(control), options->name,
+		options->format);
+
+	recording = ao2_alloc(sizeof(*recording), recording_dtor);
+	if (!recording) {
+		errno = ENOMEM;
+		return NULL;
+	}
+
+	ast_asprintf(&recording->absolute_name, "%s/%s",
+		ast_config_AST_RECORDING_DIR, options->name);
+
+	if (recording->absolute_name == NULL) {
+		errno = ENOMEM;
+		return NULL;
+	}
+
+	if ((last_slash = strrchr(recording->absolute_name, '/'))) {
+		*last_slash = '\0';
+		if (ast_safe_mkdir(ast_config_AST_RECORDING_DIR,
+				recording->absolute_name, 0777) != 0) {
+			/* errno set by ast_mkdir */
+			return NULL;
+		}
+		*last_slash = '/';
+	}
+
+	ao2_ref(options, +1);
+	recording->options = options;
+	recording->control = control;
+	recording->state = STASIS_APP_RECORDING_STATE_QUEUED;
+
+	{
+		RAII_VAR(struct stasis_app_recording *, old_recording, NULL,
+			ao2_cleanup);
+
+		SCOPED_AO2LOCK(lock, recordings);
+
+		old_recording = ao2_find(recordings, options->name,
+			OBJ_KEY | OBJ_NOLOCK);
+		if (old_recording) {
+			ast_log(LOG_WARNING,
+				"Recording %s already in progress\n",
+				recording->options->name);
+			errno = EEXIST;
+			return NULL;
+		}
+		ao2_link(recordings, recording);
+	}
+
+	/* A ref is kept in the recordings container; no need to bump */
+	stasis_app_send_command_async(control, record_file, recording);
+
+	/* Although this should be bumped for the caller */
+	ao2_ref(recording, +1);
+	return recording;
+}
+
+enum stasis_app_recording_state stasis_app_recording_get_state(
+	struct stasis_app_recording *recording)
+{
+	return recording->state;
+}
+
+const char *stasis_app_recording_get_name(
+	struct stasis_app_recording *recording)
+{
+	return recording->options->name;
+}
+
+struct stasis_app_recording *stasis_app_recording_find_by_name(const char *name)
+{
+	RAII_VAR(struct stasis_app_recording *, recording, NULL, ao2_cleanup);
+
+	recording = ao2_find(recordings, name, OBJ_KEY);
+	if (recording == NULL) {
+		return NULL;
+	}
+
+	ao2_ref(recording, +1);
+	return recording;
+}
+
+struct ast_json *stasis_app_recording_to_json(
+	const struct stasis_app_recording *recording)
+{
+	RAII_VAR(struct ast_json *, json, NULL, ast_json_unref);
+
+	if (recording == NULL) {
+		return NULL;
+	}
+
+	json = ast_json_pack("{s: s, s: s, s: s}",
+		"name", recording->options->name,
+		"format", recording->options->format,
+		"state", state_to_string(recording->state));
+
+	return ast_json_ref(json);
+}
+
+enum stasis_app_recording_oper_results stasis_app_recording_operation(
+	struct stasis_app_recording *recording,
+	enum stasis_app_recording_media_operation operation)
+{
+	ast_assert(0); // TODO
+	return STASIS_APP_RECORDING_OPER_FAILED;
+}
+
+static int load_module(void)
+{
+	int r;
+
+	r = STASIS_MESSAGE_TYPE_INIT(stasis_app_recording_snapshot_type);
+	if (r != 0) {
+		return AST_MODULE_LOAD_FAILURE;
+	}
+
+	recordings = ao2_container_alloc(RECORDING_BUCKETS, recording_hash,
+		recording_cmp);
+	if (!recordings) {
+		return AST_MODULE_LOAD_FAILURE;
+	}
+	return AST_MODULE_LOAD_SUCCESS;
+}
+
+static int unload_module(void)
+{
+	ao2_cleanup(recordings);
+	recordings = NULL;
+	STASIS_MESSAGE_TYPE_CLEANUP(stasis_app_recording_snapshot_type);
+	return 0;
+}
+
+AST_MODULE_INFO(ASTERISK_GPL_KEY, AST_MODFLAG_GLOBAL_SYMBOLS,
+	"Stasis application recording support",
+	.load = load_module,
+	.unload = unload_module,
+	.nonoptreq = "res_stasis");
diff --git a/res/res_stasis_recording.exports.in b/res/res_stasis_recording.exports.in
new file mode 100644
index 0000000000000000000000000000000000000000..0ad493c49ec08d12ba952400b7fb25a40a8dcd98
--- /dev/null
+++ b/res/res_stasis_recording.exports.in
@@ -0,0 +1,6 @@
+{
+	global:
+		LINKER_SYMBOL_PREFIXstasis_app_*;
+	local:
+		*;
+};
diff --git a/res/stasis_http/resource_channels.c b/res/stasis_http/resource_channels.c
index 0fbb754871a59208ba9e195b4f0e4831b6142088..8db3b697c1c1d554b1a88e235aa793c797c6adce 100644
--- a/res/stasis_http/resource_channels.c
+++ b/res/stasis_http/resource_channels.c
@@ -1,4 +1,4 @@
-/* -*- C -*-
+/*
  * Asterisk -- An open source telephony toolkit.
  *
  * Copyright (C) 2012 - 2013, Digium, Inc.
@@ -39,6 +39,7 @@ ASTERISK_FILE_VERSION(__FILE__, "$Revision$")
 #include "asterisk/callerid.h"
 #include "asterisk/stasis_app.h"
 #include "asterisk/stasis_app_playback.h"
+#include "asterisk/stasis_app_recording.h"
 #include "asterisk/stasis_channels.h"
 #include "resource_channels.h"
 
@@ -249,10 +250,139 @@ void stasis_http_play_on_channel(struct ast_variable *headers,
 
 	stasis_http_response_created(response, playback_url, json);
 }
-void stasis_http_record_channel(struct ast_variable *headers, struct ast_record_channel_args *args, struct stasis_http_response *response)
+
+void stasis_http_record_channel(struct ast_variable *headers,
+	struct ast_record_channel_args *args,
+	struct stasis_http_response *response)
 {
-	ast_log(LOG_ERROR, "TODO: stasis_http_record_channel\n");
+	RAII_VAR(struct stasis_app_control *, control, NULL, ao2_cleanup);
+	RAII_VAR(struct ast_channel_snapshot *, snapshot, NULL, ao2_cleanup);
+	RAII_VAR(struct stasis_app_recording *, recording, NULL, ao2_cleanup);
+	RAII_VAR(char *, recording_url, NULL, ast_free);
+	RAII_VAR(struct ast_json *, json, NULL, ast_json_unref);
+	RAII_VAR(struct stasis_app_recording_options *, options, NULL,
+		ao2_cleanup);
+	RAII_VAR(char *, uri_encoded_name, NULL, ast_free);
+	size_t uri_name_maxlen;
+
+	ast_assert(response != NULL);
+
+	if (args->max_duration_seconds < 0) {
+		stasis_http_response_error(
+			response, 400, "Bad Request",
+			"max_duration_seconds cannot be negative");
+		return;
+	}
+
+	if (args->max_silence_seconds < 0) {
+		stasis_http_response_error(
+			response, 400, "Bad Request",
+			"max_silence_seconds cannot be negative");
+		return;
+	}
+
+	control = find_control(response, args->channel_id);
+	if (control == NULL) {
+		/* Response filled in by find_control */
+		return;
+	}
+
+	options = stasis_app_recording_options_create(args->name, args->format);
+	if (options == NULL) {
+		stasis_http_response_error(
+			response, 500, "Internal Server Error",
+			"Out of memory");
+	}
+	options->max_silence_seconds = args->max_silence_seconds;
+	options->max_duration_seconds = args->max_duration_seconds;
+	options->terminate_on =
+		stasis_app_recording_termination_parse(args->terminate_on);
+	options->if_exists =
+		stasis_app_recording_if_exists_parse(args->if_exists);
+	options->beep = args->beep;
+
+	if (options->terminate_on == STASIS_APP_RECORDING_TERMINATE_INVALID) {
+		stasis_http_response_error(
+			response, 400, "Bad Request",
+			"terminateOn invalid");
+		return;
+	}
+
+	if (options->if_exists == -1) {
+		stasis_http_response_error(
+			response, 400, "Bad Request",
+			"ifExists invalid");
+		return;
+	}
+
+	recording = stasis_app_control_record(control, options);
+	if (recording == NULL) {
+		switch(errno) {
+		case EINVAL:
+			/* While the arguments are invalid, we should have
+			 * caught them prior to calling record.
+			 */
+			stasis_http_response_error(
+				response, 500, "Internal Server Error",
+				"Error parsing request");
+			break;
+		case EEXIST:
+			stasis_http_response_error(response, 409, "Conflict",
+				"Recording '%s' already in progress",
+				args->name);
+			break;
+		case ENOMEM:
+			stasis_http_response_error(
+				response, 500, "Internal Server Error",
+				"Out of memory");
+			break;
+		case EPERM:
+			stasis_http_response_error(
+				response, 400, "Bad Request",
+				"Recording name invalid");
+			break;
+		default:
+			ast_log(LOG_WARNING,
+				"Unrecognized recording error: %s\n",
+				strerror(errno));
+			stasis_http_response_error(
+				response, 500, "Internal Server Error",
+				"Internal Server Error");
+			break;
+		}
+		return;
+	}
+
+	uri_name_maxlen = strlen(args->name) * 3;
+	uri_encoded_name = ast_malloc(uri_name_maxlen);
+	if (!uri_encoded_name) {
+		stasis_http_response_error(
+			response, 500, "Internal Server Error",
+			"Out of memory");
+		return;
+	}
+	ast_uri_encode(args->name, uri_encoded_name, uri_name_maxlen,
+		ast_uri_http);
+
+	ast_asprintf(&recording_url, "/recordings/live/%s", uri_encoded_name);
+	if (!recording_url) {
+		stasis_http_response_error(
+			response, 500, "Internal Server Error",
+			"Out of memory");
+		return;
+	}
+
+	json = stasis_app_recording_to_json(recording);
+	if (!json) {
+		stasis_http_response_error(
+			response, 500, "Internal Server Error",
+			"Out of memory");
+		return;
+	}
+
+	stasis_http_response_created(response, recording_url, json);
 }
+
 void stasis_http_get_channel(struct ast_variable *headers,
 			     struct ast_get_channel_args *args,
 			     struct stasis_http_response *response)
diff --git a/res/stasis_http/resource_channels.h b/res/stasis_http/resource_channels.h
index 57f2a63d20821cf35be66eb71d1faee5ab1dbb9d..7e8dc5dbe6c79ee8d6bc2c1660f940ae9e12b16b 100644
--- a/res/stasis_http/resource_channels.h
+++ b/res/stasis_http/resource_channels.h
@@ -247,8 +247,8 @@ struct ast_record_channel_args {
 	int max_duration_seconds;
 	/*! \brief Maximum duration of silence, in seconds. 0 for no limit */
 	int max_silence_seconds;
-	/*! \brief If true, and recording already exists, append to recording */
-	int append;
+	/*! \brief Action to take if a recording with the same name already exists. */
+	const char *if_exists;
 	/*! \brief Play beep when recording begins */
 	int beep;
 	/*! \brief DTMF input to terminate recording */
diff --git a/res/stasis_http/resource_recordings.c b/res/stasis_http/resource_recordings.c
index 7d31c42aa20faa80d749618cacc36ea9a3244aa7..d93d59017c1ea9140f6663211d6f8d48914dbe7a 100644
--- a/res/stasis_http/resource_recordings.c
+++ b/res/stasis_http/resource_recordings.c
@@ -27,6 +27,7 @@
 
 ASTERISK_FILE_VERSION(__FILE__, "$Revision$")
 
+#include "asterisk/stasis_app_recording.h"
 #include "resource_recordings.h"
 
 void stasis_http_get_stored_recordings(struct ast_variable *headers, struct ast_get_stored_recordings_args *args, struct stasis_http_response *response)
@@ -45,10 +46,31 @@ void stasis_http_get_live_recordings(struct ast_variable *headers, struct ast_ge
 {
 	ast_log(LOG_ERROR, "TODO: stasis_http_get_live_recordings\n");
 }
-void stasis_http_get_live_recording(struct ast_variable *headers, struct ast_get_live_recording_args *args, struct stasis_http_response *response)
+
+void stasis_http_get_live_recording(struct ast_variable *headers,
+	struct ast_get_live_recording_args *args,
+	struct stasis_http_response *response)
 {
-	ast_log(LOG_ERROR, "TODO: stasis_http_get_live_recording\n");
+	RAII_VAR(struct stasis_app_recording *, recording, NULL, ao2_cleanup);
+	RAII_VAR(struct ast_json *, json, NULL, ast_json_unref);
+
+	recording = stasis_app_recording_find_by_name(args->recording_name);
+	if (recording == NULL) {
+		stasis_http_response_error(response, 404, "Not Found",
+			"Recording not found");
+		return;
+	}
+
+	json = stasis_app_recording_to_json(recording);
+	if (json == NULL) {
+		stasis_http_response_error(response, 500,
+			"Internal Server Error", "Error building response");
+		return;
+	}
+
+	stasis_http_response_ok(response, ast_json_ref(json));
 }
+
 void stasis_http_cancel_recording(struct ast_variable *headers, struct ast_cancel_recording_args *args, struct stasis_http_response *response)
 {
 	ast_log(LOG_ERROR, "TODO: stasis_http_cancel_recording\n");
diff --git a/res/stasis_http/resource_recordings.h b/res/stasis_http/resource_recordings.h
index acccc124bb1ed6acfa1f85ce17cb759349e0a69e..18a5bfe68398eec97dc08a83ebbefd4ef3d49789 100644
--- a/res/stasis_http/resource_recordings.h
+++ b/res/stasis_http/resource_recordings.h
@@ -52,8 +52,8 @@ struct ast_get_stored_recordings_args {
 void stasis_http_get_stored_recordings(struct ast_variable *headers, struct ast_get_stored_recordings_args *args, struct stasis_http_response *response);
 /*! \brief Argument struct for stasis_http_get_stored_recording() */
 struct ast_get_stored_recording_args {
-	/*! \brief Recording's id */
-	const char *recording_id;
+	/*! \brief The name of the recording */
+	const char *recording_name;
 };
 /*!
  * \brief Get a stored recording's details.
@@ -65,8 +65,8 @@ struct ast_get_stored_recording_args {
 void stasis_http_get_stored_recording(struct ast_variable *headers, struct ast_get_stored_recording_args *args, struct stasis_http_response *response);
 /*! \brief Argument struct for stasis_http_delete_stored_recording() */
 struct ast_delete_stored_recording_args {
-	/*! \brief Recording's id */
-	const char *recording_id;
+	/*! \brief The name of the recording */
+	const char *recording_name;
 };
 /*!
  * \brief Delete a stored recording.
@@ -89,8 +89,8 @@ struct ast_get_live_recordings_args {
 void stasis_http_get_live_recordings(struct ast_variable *headers, struct ast_get_live_recordings_args *args, struct stasis_http_response *response);
 /*! \brief Argument struct for stasis_http_get_live_recording() */
 struct ast_get_live_recording_args {
-	/*! \brief Recording's id */
-	const char *recording_id;
+	/*! \brief The name of the recording */
+	const char *recording_name;
 };
 /*!
  * \brief List live recordings.
@@ -102,8 +102,8 @@ struct ast_get_live_recording_args {
 void stasis_http_get_live_recording(struct ast_variable *headers, struct ast_get_live_recording_args *args, struct stasis_http_response *response);
 /*! \brief Argument struct for stasis_http_cancel_recording() */
 struct ast_cancel_recording_args {
-	/*! \brief Recording's id */
-	const char *recording_id;
+	/*! \brief The name of the recording */
+	const char *recording_name;
 };
 /*!
  * \brief Stop a live recording and discard it.
@@ -115,8 +115,8 @@ struct ast_cancel_recording_args {
 void stasis_http_cancel_recording(struct ast_variable *headers, struct ast_cancel_recording_args *args, struct stasis_http_response *response);
 /*! \brief Argument struct for stasis_http_stop_recording() */
 struct ast_stop_recording_args {
-	/*! \brief Recording's id */
-	const char *recording_id;
+	/*! \brief The name of the recording */
+	const char *recording_name;
 };
 /*!
  * \brief Stop a live recording and store it.
@@ -128,12 +128,14 @@ struct ast_stop_recording_args {
 void stasis_http_stop_recording(struct ast_variable *headers, struct ast_stop_recording_args *args, struct stasis_http_response *response);
 /*! \brief Argument struct for stasis_http_pause_recording() */
 struct ast_pause_recording_args {
-	/*! \brief Recording's id */
-	const char *recording_id;
+	/*! \brief The name of the recording */
+	const char *recording_name;
 };
 /*!
  * \brief Pause a live recording.
  *
+ * Pausing a recording suspends silence detection, which will be restarted when the recording is unpaused.
+ *
  * \param headers HTTP headers
  * \param args Swagger parameters
  * \param[out] response HTTP response
@@ -141,8 +143,8 @@ struct ast_pause_recording_args {
 void stasis_http_pause_recording(struct ast_variable *headers, struct ast_pause_recording_args *args, struct stasis_http_response *response);
 /*! \brief Argument struct for stasis_http_unpause_recording() */
 struct ast_unpause_recording_args {
-	/*! \brief Recording's id */
-	const char *recording_id;
+	/*! \brief The name of the recording */
+	const char *recording_name;
 };
 /*!
  * \brief Unpause a live recording.
@@ -154,12 +156,14 @@ struct ast_unpause_recording_args {
 void stasis_http_unpause_recording(struct ast_variable *headers, struct ast_unpause_recording_args *args, struct stasis_http_response *response);
 /*! \brief Argument struct for stasis_http_mute_recording() */
 struct ast_mute_recording_args {
-	/*! \brief Recording's id */
-	const char *recording_id;
+	/*! \brief The name of the recording */
+	const char *recording_name;
 };
 /*!
  * \brief Mute a live recording.
  *
+ * Muting a recording suspends silence detection, which will be restarted when the recording is unmuted.
+ *
  * \param headers HTTP headers
  * \param args Swagger parameters
  * \param[out] response HTTP response
@@ -167,8 +171,8 @@ struct ast_mute_recording_args {
 void stasis_http_mute_recording(struct ast_variable *headers, struct ast_mute_recording_args *args, struct stasis_http_response *response);
 /*! \brief Argument struct for stasis_http_unmute_recording() */
 struct ast_unmute_recording_args {
-	/*! \brief Recording's id */
-	const char *recording_id;
+	/*! \brief The name of the recording */
+	const char *recording_name;
 };
 /*!
  * \brief Unmute a live recording.
diff --git a/rest-api-templates/asterisk_processor.py b/rest-api-templates/asterisk_processor.py
index 0260b6b55bab02c80088062afc57804df7e327b1..6f69b48659303827be03983ec85f6c3808e2dcc1 100644
--- a/rest-api-templates/asterisk_processor.py
+++ b/rest-api-templates/asterisk_processor.py
@@ -139,10 +139,11 @@ class AsteriskProcessor(SwaggerPostProcessor):
 
     #: String conversion functions for string to C type.
     convert_mapping = {
-        'const char *': '',
+        'string': '',
         'int': 'atoi',
         'long': 'atol',
         'double': 'atof',
+        'boolean': 'ast_true',
     }
 
     def __init__(self, wiki_prefix):
@@ -194,7 +195,7 @@ class AsteriskProcessor(SwaggerPostProcessor):
         # Parameter names are camelcase, Asterisk convention is snake case
         parameter.c_name = snakify(parameter.name)
         parameter.c_data_type = self.type_mapping[parameter.data_type]
-        parameter.c_convert = self.convert_mapping[parameter.c_data_type]
+        parameter.c_convert = self.convert_mapping[parameter.data_type]
         # You shouldn't put a space between 'char *' and the variable
         if parameter.c_data_type.endswith('*'):
             parameter.c_space = ''
diff --git a/rest-api-templates/swagger_model.py b/rest-api-templates/swagger_model.py
index 2907688c5231680a2584a32f2c147f1ef3462e5e..aa065b342bf708531cbe7d8356950b846ed5a063 100644
--- a/rest-api-templates/swagger_model.py
+++ b/rest-api-templates/swagger_model.py
@@ -246,11 +246,9 @@ def load_allowable_values(json, context):
     value_type = json['valueType']
 
     if value_type == 'RANGE':
-        if not 'min' in json:
-            raise SwaggerError("Missing field min", context)
-        if not 'max' in json:
-            raise SwaggerError("Missing field max", context)
-        return AllowableRange(json['min'], json['max'])
+        if not 'min' in json and not 'max' in json:
+            raise SwaggerError("Missing fields min/max", context)
+        return AllowableRange(json.get('min'), json.get('max'))
     if value_type == 'LIST':
         if not 'values' in json:
             raise SwaggerError("Missing field values", context)
diff --git a/rest-api/api-docs/channels.json b/rest-api/api-docs/channels.json
index f013ef6416d3c97242b01cfd59c8b5d36b89bb71..9900db7394b05a342cdc1e5183c702813855409e 100644
--- a/rest-api/api-docs/channels.json
+++ b/rest-api/api-docs/channels.json
@@ -565,7 +565,11 @@
 							"required": false,
 							"allowMultiple": false,
 							"dataType": "int",
-							"defaultValue": 0
+							"defaultValue": 0,
+							"allowableValues": {
+								"valueType": "RANGE",
+								"min": 0
+							}
 						},
 						{
 							"name": "maxSilenceSeconds",
@@ -574,16 +578,28 @@
 							"required": false,
 							"allowMultiple": false,
 							"dataType": "int",
-							"defaultValue": 0
+							"defaultValue": 0,
+							"allowableValues": {
+								"valueType": "RANGE",
+								"min": 0
+							}
 						},
 						{
-							"name": "append",
-							"description": "If true, and recording already exists, append to recording",
+							"name": "ifExists",
+							"description": "Action to take if a recording with the same name already exists.",
 							"paramType": "query",
 							"required": false,
 							"allowMultiple": false,
-							"dataType": "boolean",
-							"defaultValue": false
+							"dataType": "string",
+							"defaultValue": "fail",
+							"allowableValues": {
+								"valueType": "LIST",
+								"values": [
+									"fail",
+									"overwrite",
+									"append"
+								]
+							}
 						},
 						{
 							"name": "beep",
@@ -614,13 +630,17 @@
 						}
 					],
 					"errorResponses": [
+						{
+							"code": 400,
+							"reason": "Invalid parameters"
+						},
 						{
 							"code": 404,
 							"reason": "Channel not found"
 						},
 						{
 							"code": 409,
-							"reason": "Channel is not in a Stasis application, or the channel is currently bridged with other channels."
+							"reason": "Channel is not in a Stasis application; the channel is currently bridged with other channels; A recording with the same name is currently in progress."
 						}
 					]
 				}
diff --git a/rest-api/api-docs/recordings.json b/rest-api/api-docs/recordings.json
index ce11d17c25edb3d7c9ec9f65275673a01b21b89f..9efdc7bb31dca82c5b5857d4f3018a533818bc6a 100644
--- a/rest-api/api-docs/recordings.json
+++ b/rest-api/api-docs/recordings.json
@@ -20,7 +20,7 @@
 			]
 		},
 		{
-			"path": "/recordings/stored/{recordingId}",
+			"path": "/recordings/stored/{recordingName}",
 			"description": "Individual recording",
 			"operations": [
 				{
@@ -30,8 +30,8 @@
 					"responseClass": "StoredRecording",
 					"parameters": [
 						{
-							"name": "recordingId",
-							"description": "Recording's id",
+							"name": "recordingName",
+							"description": "The name of the recording",
 							"paramType": "path",
 							"required": true,
 							"allowMultiple": false,
@@ -46,8 +46,8 @@
 					"responseClass": "void",
 					"parameters": [
 						{
-							"name": "recordingId",
-							"description": "Recording's id",
+							"name": "recordingName",
+							"description": "The name of the recording",
 							"paramType": "path",
 							"required": true,
 							"allowMultiple": false,
@@ -70,7 +70,7 @@
 			]
 		},
 		{
-			"path": "/recordings/live/{recordingId}",
+			"path": "/recordings/live/{recordingName}",
 			"description": "A recording that is in progress",
 			"operations": [
 				{
@@ -80,8 +80,8 @@
 					"responseClass": "LiveRecording",
 					"parameters": [
 						{
-							"name": "recordingId",
-							"description": "Recording's id",
+							"name": "recordingName",
+							"description": "The name of the recording",
 							"paramType": "path",
 							"required": true,
 							"allowMultiple": false,
@@ -96,8 +96,8 @@
 					"responseClass": "void",
 					"parameters": [
 						{
-							"name": "recordingId",
-							"description": "Recording's id",
+							"name": "recordingName",
+							"description": "The name of the recording",
 							"paramType": "path",
 							"required": true,
 							"allowMultiple": false,
@@ -108,7 +108,7 @@
 			]
 		},
 		{
-			"path": "/recordings/live/{recordingId}/stop",
+			"path": "/recordings/live/{recordingName}/stop",
 			"operations": [
 				{
 					"httpMethod": "POST",
@@ -117,8 +117,8 @@
 					"responseClass": "void",
 					"parameters": [
 						{
-							"name": "recordingId",
-							"description": "Recording's id",
+							"name": "recordingName",
+							"description": "The name of the recording",
 							"paramType": "path",
 							"required": true,
 							"allowMultiple": false,
@@ -129,17 +129,18 @@
 			]
 		},
 		{
-			"path": "/recordings/live/{recordingId}/pause",
+			"path": "/recordings/live/{recordingName}/pause",
 			"operations": [
 				{
 					"httpMethod": "POST",
 					"summary": "Pause a live recording.",
+					"notes": "Pausing a recording suspends silence detection, which will be restarted when the recording is unpaused.",
 					"nickname": "pauseRecording",
 					"responseClass": "void",
 					"parameters": [
 						{
-							"name": "recordingId",
-							"description": "Recording's id",
+							"name": "recordingName",
+							"description": "The name of the recording",
 							"paramType": "path",
 							"required": true,
 							"allowMultiple": false,
@@ -150,7 +151,7 @@
 			]
 		},
 		{
-			"path": "/recordings/live/{recordingId}/unpause",
+			"path": "/recordings/live/{recordingName}/unpause",
 			"operations": [
 				{
 					"httpMethod": "POST",
@@ -159,8 +160,8 @@
 					"responseClass": "void",
 					"parameters": [
 						{
-							"name": "recordingId",
-							"description": "Recording's id",
+							"name": "recordingName",
+							"description": "The name of the recording",
 							"paramType": "path",
 							"required": true,
 							"allowMultiple": false,
@@ -171,17 +172,18 @@
 			]
 		},
 		{
-			"path": "/recordings/live/{recordingId}/mute",
+			"path": "/recordings/live/{recordingName}/mute",
 			"operations": [
 				{
 					"httpMethod": "POST",
 					"summary": "Mute a live recording.",
+					"notes": "Muting a recording suspends silence detection, which will be restarted when the recording is unmuted.",
 					"nickname": "muteRecording",
 					"responseClass": "void",
 					"parameters": [
 						{
-							"name": "recordingId",
-							"description": "Recording's id",
+							"name": "recordingName",
+							"description": "The name of the recording",
 							"paramType": "path",
 							"required": true,
 							"allowMultiple": false,
@@ -192,7 +194,7 @@
 			]
 		},
 		{
-			"path": "/recordings/live/{recordingId}/unmute",
+			"path": "/recordings/live/{recordingName}/unmute",
 			"operations": [
 				{
 					"httpMethod": "POST",
@@ -201,8 +203,8 @@
 					"responseClass": "void",
 					"parameters": [
 						{
-							"name": "recordingId",
-							"description": "Recording's id",
+							"name": "recordingName",
+							"description": "The name of the recording",
 							"paramType": "path",
 							"required": true,
 							"allowMultiple": false,
diff --git a/tests/test_utils.c b/tests/test_utils.c
index 7cc4cf611b8f7cd63e6e4203af24c4765a36e68f..f956e5b27be2e294da560a70dce17fcaed46709e 100644
--- a/tests/test_utils.c
+++ b/tests/test_utils.c
@@ -42,6 +42,8 @@ ASTERISK_FILE_VERSION(__FILE__, "$Revision$");
 #include "asterisk/channel.h"
 #include "asterisk/module.h"
 
+#include <sys/stat.h>
+
 AST_TEST_DEFINE(uri_encode_decode_test)
 {
 	int res = AST_TEST_PASS;
@@ -421,6 +423,93 @@ AST_TEST_DEFINE(agi_loaded_test)
 	return res;
 }
 
+AST_TEST_DEFINE(safe_mkdir_test)
+{
+	char base_path[] = "/tmp/safe_mkdir.XXXXXX";
+	char path[80] = {};
+	int res;
+	struct stat actual;
+
+	switch (cmd) {
+	case TEST_INIT:
+		info->name = __func__;
+		info->category = "/main/utils/";
+		info->summary = "Safe mkdir test";
+		info->description =
+			"This test ensures that ast_safe_mkdir does what it is "
+			"supposed to";
+		return AST_TEST_NOT_RUN;
+	case TEST_EXECUTE:
+		break;
+	}
+
+	if (mkdtemp(base_path) == NULL) {
+		ast_test_status_update(test, "Failed to create tmpdir for test\n");
+		return AST_TEST_FAIL;
+	}
+
+	snprintf(path, sizeof(path), "%s/should_work", base_path);
+	res = ast_safe_mkdir(base_path, path, 0777);
+	ast_test_validate(test, 0 == res);
+	res = stat(path, &actual);
+	ast_test_validate(test, 0 == res);
+	ast_test_validate(test, S_ISDIR(actual.st_mode));
+
+	snprintf(path, sizeof(path), "%s/should/also/work", base_path);
+	res = ast_safe_mkdir(base_path, path, 0777);
+	ast_test_validate(test, 0 == res);
+	res = stat(path, &actual);
+	ast_test_validate(test, 0 == res);
+	ast_test_validate(test, S_ISDIR(actual.st_mode));
+
+	snprintf(path, sizeof(path), "%s/even/this/../should/work", base_path);
+	res = ast_safe_mkdir(base_path, path, 0777);
+	ast_test_validate(test, 0 == res);
+	snprintf(path, sizeof(path), "%s/even/should/work", base_path);
+	res = stat(path, &actual);
+	ast_test_validate(test, 0 == res);
+	ast_test_validate(test, S_ISDIR(actual.st_mode));
+
+	snprintf(path, sizeof(path),
+		"%s/surprisingly/this/should//////////////////work", base_path);
+	res = ast_safe_mkdir(base_path, path, 0777);
+	ast_test_validate(test, 0 == res);
+	snprintf(path, sizeof(path),
+		"%s/surprisingly/this/should/work", base_path);
+	res = stat(path, &actual);
+	ast_test_validate(test, 0 == res);
+	ast_test_validate(test, S_ISDIR(actual.st_mode));
+
+	snprintf(path, sizeof(path), "/should_not_work");
+	res = ast_safe_mkdir(base_path, path, 0777);
+	ast_test_validate(test, 0 != res);
+	ast_test_validate(test, EPERM == errno);
+	res = stat(path, &actual);
+	ast_test_validate(test, 0 != res);
+	ast_test_validate(test, ENOENT == errno);
+
+	snprintf(path, sizeof(path), "%s/../nor_should_this", base_path);
+	res = ast_safe_mkdir(base_path, path, 0777);
+	ast_test_validate(test, 0 != res);
+	ast_test_validate(test, EPERM == errno);
+	strncpy(path, "/tmp/nor_should_this", sizeof(path));
+	res = stat(path, &actual);
+	ast_test_validate(test, 0 != res);
+	ast_test_validate(test, ENOENT == errno);
+
+	snprintf(path, sizeof(path),
+		"%s/this/especially/should/not/../../../../../work", base_path);
+	res = ast_safe_mkdir(base_path, path, 0777);
+	ast_test_validate(test, 0 != res);
+	ast_test_validate(test, EPERM == errno);
+	strncpy(path, "/tmp/work", sizeof(path));
+	res = stat(path, &actual);
+	ast_test_validate(test, 0 != res);
+	ast_test_validate(test, ENOENT == errno);
+
+	return AST_TEST_PASS;
+}
+
 AST_TEST_DEFINE(crypt_test)
 {
 	RAII_VAR(char *, password_crypted, NULL, ast_free);
@@ -467,6 +556,7 @@ static int unload_module(void)
 	AST_TEST_UNREGISTER(crypto_loaded_test);
 	AST_TEST_UNREGISTER(adsi_loaded_test);
 	AST_TEST_UNREGISTER(agi_loaded_test);
+	AST_TEST_UNREGISTER(safe_mkdir_test);
 	AST_TEST_UNREGISTER(crypt_test);
 	return 0;
 }
@@ -481,6 +571,7 @@ static int load_module(void)
 	AST_TEST_REGISTER(crypto_loaded_test);
 	AST_TEST_REGISTER(adsi_loaded_test);
 	AST_TEST_REGISTER(agi_loaded_test);
+	AST_TEST_REGISTER(safe_mkdir_test);
 	AST_TEST_REGISTER(crypt_test);
 	return AST_MODULE_LOAD_SUCCESS;
 }