diff --git a/src/wp-admin/js/widgets/media-video-widget.js b/src/wp-admin/js/widgets/media-video-widget.js
index 07b203f..9eb532c 100644
--- a/src/wp-admin/js/widgets/media-video-widget.js
+++ b/src/wp-admin/js/widgets/media-video-widget.js
@@ -110,13 +110,12 @@
 				control.fetchEmbedDfd.abort();
 			}
 
-			control.fetchEmbedDfd = jQuery.ajax({
+			control.fetchEmbedDfd = wp.apiRequest({
 				url: wp.media.view.settings.oEmbedProxyUrl,
 				data: {
 					url: control.model.get( 'url' ),
 					maxwidth: control.model.get( 'width' ),
 					maxheight: control.model.get( 'height' ),
-					_wpnonce: wp.media.view.settings.nonce.wpRestApi,
 					discover: false
 				},
 				type: 'GET',
diff --git a/src/wp-admin/js/widgets/media-widgets.js b/src/wp-admin/js/widgets/media-widgets.js
index e922670..c4f23e1 100644
--- a/src/wp-admin/js/widgets/media-widgets.js
+++ b/src/wp-admin/js/widgets/media-widgets.js
@@ -200,13 +200,12 @@ wp.mediaWidgets = ( function( $ ) {
 							embedLinkView.model.attributes.url = url;
 						}
 
-						embedLinkView.dfd = $.ajax({
+						embedLinkView.dfd = wp.apiRequest({
 							url: wp.media.view.settings.oEmbedProxyUrl,
 							data: {
 								url: url,
 								maxwidth: embedLinkView.model.get( 'width' ),
 								maxheight: embedLinkView.model.get( 'height' ),
-								_wpnonce: wp.media.view.settings.nonce.wpRestApi,
 								discover: false
 							},
 							type: 'GET',
diff --git a/src/wp-includes/js/api-request.js b/src/wp-includes/js/api-request.js
new file mode 100644
index 0000000..2b4ac4b
--- /dev/null
+++ b/src/wp-includes/js/api-request.js
@@ -0,0 +1,87 @@
+/**
+ * Thin jQuery.ajax wrapper for WP REST API requests.
+ *
+ * Currently only applies to requests that do not use the `wp-api.js` Backbone
+ * client library, though this may change.  Serves several purposes:
+ *
+ * - Allows overriding these requests as needed by customized WP installations.
+ * - Sends the REST API nonce as a request header.
+ * - Allows specifying only an endpoint namespace/path instead of a full URL.
+ *
+ * @namespace wp.apiRequest
+ * @since     4.9.0
+ */
+
+( function( $ ) {
+	var wpApiSettings = window.wpApiSettings;
+
+	function apiRequest( options ) {
+		options = apiRequest.buildAjaxOptions( options );
+		return apiRequest.transport( options );
+	};
+
+	apiRequest.buildAjaxOptions = function( options ) {
+		var url = options.url;
+		var path = options.path;
+		var namespaceTrimmed, endpointTrimmed;
+		var headers, addNonceHeader, headerName;
+
+		if (
+			typeof options.namespace === 'string' &&
+			typeof options.endpoint === 'string'
+		) {
+			namespaceTrimmed = options.namespace.replace( /^\/|\/$/g, '' );
+			endpointTrimmed = options.endpoint.replace( /^\//, '' );
+			if ( endpointTrimmed ) {
+				path = namespaceTrimmed + '/' + endpointTrimmed;
+			} else {
+				path = namespaceTrimmed;
+			}
+		}
+		if ( typeof path === 'string' ) {
+			url = wpApiSettings.root + path.replace( /^\//, '' );
+		}
+
+		// If ?_wpnonce=... is present, no need to add a nonce header.
+		addNonceHeader = ! ( options.data && options.data._wpnonce );
+
+		headers = options.headers || {};
+
+		// If an 'X-WP-Nonce' header (or any case-insensitive variation
+		// thereof) was specified, no need to add a nonce header.
+		if ( addNonceHeader ) {
+			for ( headerName in headers ) {
+				if ( headers.hasOwnProperty( headerName ) ) {
+					if ( headerName.toLowerCase() === 'x-wp-nonce' ) {
+						addNonceHeader = false;
+						break;
+					}
+				}
+			}
+		}
+
+		if ( addNonceHeader ) {
+			// Do not mutate the original headers object, if any.
+			headers = $.extend( {
+				'X-WP-Nonce': wpApiSettings.nonce
+			}, headers );
+		}
+
+		// Do not mutate the original options object.
+		options = $.extend( {}, options, {
+			headers: headers,
+			url: url
+		} );
+
+		delete options.path;
+		delete options.namespace;
+		delete options.endpoint;
+
+		return options;
+	};
+
+	apiRequest.transport = $.ajax;
+
+	window.wp = window.wp || {};
+	window.wp.apiRequest = apiRequest;
+} )( jQuery );
diff --git a/src/wp-includes/js/media-views.js b/src/wp-includes/js/media-views.js
index 991b3d0..6d46229 100644
--- a/src/wp-includes/js/media-views.js
+++ b/src/wp-includes/js/media-views.js
@@ -4642,13 +4642,12 @@ EmbedLink = wp.media.view.Settings.extend({
 			url = 'https://www.youtube.com/watch?v=' + youTubeEmbedMatch[ 1 ];
 		}
 
-		this.dfd = $.ajax({
+		this.dfd = wp.apiRequest({
 			url: wp.media.view.settings.oEmbedProxyUrl,
 			data: {
 				url: url,
 				maxwidth: this.model.get( 'width' ),
-				maxheight: this.model.get( 'height' ),
-				_wpnonce: wp.media.view.settings.nonce.wpRestApi
+				maxheight: this.model.get( 'height' )
 			},
 			type: 'GET',
 			dataType: 'json',
diff --git a/src/wp-includes/js/media/views/embed/link.js b/src/wp-includes/js/media/views/embed/link.js
index 36925f4..12126d8 100644
--- a/src/wp-includes/js/media/views/embed/link.js
+++ b/src/wp-includes/js/media/views/embed/link.js
@@ -53,13 +53,12 @@ EmbedLink = wp.media.view.Settings.extend({
 			url = 'https://www.youtube.com/watch?v=' + youTubeEmbedMatch[ 1 ];
 		}
 
-		this.dfd = $.ajax({
+		this.dfd = wp.apiRequest({
 			url: wp.media.view.settings.oEmbedProxyUrl,
 			data: {
 				url: url,
 				maxwidth: this.model.get( 'width' ),
-				maxheight: this.model.get( 'height' ),
-				_wpnonce: wp.media.view.settings.nonce.wpRestApi
+				maxheight: this.model.get( 'height' )
 			},
 			type: 'GET',
 			dataType: 'json',
diff --git a/src/wp-includes/media.php b/src/wp-includes/media.php
index 81dc502..6c4fce4 100644
--- a/src/wp-includes/media.php
+++ b/src/wp-includes/media.php
@@ -3437,7 +3437,6 @@ function wp_enqueue_media( $args = array() ) {
 		'captions'  => ! apply_filters( 'disable_captions', '' ),
 		'nonce'     => array(
 			'sendToEditor' => wp_create_nonce( 'media-send-to-editor' ),
-			'wpRestApi'    => wp_create_nonce( 'wp_rest' ),
 		),
 		'post'    => array(
 			'id' => 0,
diff --git a/src/wp-includes/script-loader.php b/src/wp-includes/script-loader.php
index 8858984..3c3659e 100644
--- a/src/wp-includes/script-loader.php
+++ b/src/wp-includes/script-loader.php
@@ -133,6 +133,14 @@ function wp_default_scripts( &$scripts ) {
 		'broken' => __('An unidentified error has occurred.')
 	) );
 
+	$scripts->add( 'wp-api-request', "/wp-includes/js/api-request$suffix.js", array( 'jquery' ), false, 1 );
+	// `wpApiSettings` is also used by `wp-api`, which depends on this script.
+	did_action( 'init' ) && $scripts->localize( 'wp-api-request', 'wpApiSettings', array(
+		'root'          => esc_url_raw( get_rest_url() ),
+		'nonce'         => ( wp_installing() && ! is_multisite() ) ? '' : wp_create_nonce( 'wp_rest' ),
+		'versionString' => 'wp/v2/',
+	) );
+
 	$scripts->add( 'wp-pointer', "/wp-includes/js/wp-pointer$suffix.js", array( 'jquery-ui-widget', 'jquery-ui-position' ), '20111129a', 1 );
 	did_action( 'init' ) && $scripts->localize( 'wp-pointer', 'wpPointerL10n', array(
 		'dismiss' => __('Dismiss'),
@@ -566,17 +574,12 @@ function wp_default_scripts( &$scripts ) {
 
 	// To enqueue media-views or media-editor, call wp_enqueue_media().
 	// Both rely on numerous settings, styles, and templates to operate correctly.
-	$scripts->add( 'media-views',  "/wp-includes/js/media-views$suffix.js",  array( 'utils', 'media-models', 'wp-plupload', 'jquery-ui-sortable', 'wp-mediaelement' ), false, 1 );
+	$scripts->add( 'media-views',  "/wp-includes/js/media-views$suffix.js",  array( 'utils', 'media-models', 'wp-plupload', 'jquery-ui-sortable', 'wp-mediaelement', 'wp-api-request' ), false, 1 );
 	$scripts->add( 'media-editor', "/wp-includes/js/media-editor$suffix.js", array( 'shortcode', 'media-views' ), false, 1 );
 	$scripts->add( 'media-audiovideo', "/wp-includes/js/media-audiovideo$suffix.js", array( 'media-editor' ), false, 1 );
 	$scripts->add( 'mce-view', "/wp-includes/js/mce-view$suffix.js", array( 'shortcode', 'jquery', 'media-views', 'media-audiovideo' ), false, 1 );
 
-	$scripts->add( 'wp-api', "/wp-includes/js/wp-api$suffix.js", array( 'jquery', 'backbone', 'underscore' ), false, 1 );
-	did_action( 'init' ) && $scripts->localize( 'wp-api', 'wpApiSettings', array(
-		'root'          => esc_url_raw( get_rest_url() ),
-		'nonce'         => ( wp_installing() && ! is_multisite() ) ? '' : wp_create_nonce( 'wp_rest' ),
-		'versionString' => 'wp/v2/',
-	) );
+	$scripts->add( 'wp-api', "/wp-includes/js/wp-api$suffix.js", array( 'jquery', 'backbone', 'underscore', 'wp-api-request' ), false, 1 );
 
 	if ( is_admin() ) {
 		$scripts->add( 'admin-tags', "/wp-admin/js/tags$suffix.js", array( 'jquery', 'wp-ajax-response' ), false, 1 );
@@ -669,12 +672,12 @@ function wp_default_scripts( &$scripts ) {
 		$scripts->add( 'admin-gallery', "/wp-admin/js/gallery$suffix.js", array( 'jquery-ui-sortable' ) );
 
 		$scripts->add( 'admin-widgets', "/wp-admin/js/widgets$suffix.js", array( 'jquery-ui-sortable', 'jquery-ui-draggable', 'jquery-ui-droppable' ), false, 1 );
-		$scripts->add( 'media-widgets', "/wp-admin/js/widgets/media-widgets$suffix.js", array( 'jquery', 'media-models', 'media-views' ) );
+		$scripts->add( 'media-widgets', "/wp-admin/js/widgets/media-widgets$suffix.js", array( 'jquery', 'media-models', 'media-views', 'wp-api-request' ) );
 		$scripts->add_inline_script( 'media-widgets', 'wp.mediaWidgets.init();', 'after' );
 
 		$scripts->add( 'media-audio-widget', "/wp-admin/js/widgets/media-audio-widget$suffix.js", array( 'media-widgets', 'media-audiovideo' ) );
 		$scripts->add( 'media-image-widget', "/wp-admin/js/widgets/media-image-widget$suffix.js", array( 'media-widgets' ) );
-		$scripts->add( 'media-video-widget', "/wp-admin/js/widgets/media-video-widget$suffix.js", array( 'media-widgets', 'media-audiovideo' ) );
+		$scripts->add( 'media-video-widget', "/wp-admin/js/widgets/media-video-widget$suffix.js", array( 'media-widgets', 'media-audiovideo', 'wp-api-request' ) );
 		$scripts->add( 'text-widgets', "/wp-admin/js/widgets/text-widgets$suffix.js", array( 'jquery', 'backbone', 'editor', 'wp-util', 'wp-a11y' ) );
 		$scripts->add_inline_script( 'text-widgets', 'wp.textWidgets.init();', 'after' );
 
diff --git a/tests/qunit/index.html b/tests/qunit/index.html
index c41fffe..2ca82b3 100644
--- a/tests/qunit/index.html
+++ b/tests/qunit/index.html
@@ -19,7 +19,8 @@
 		</script>
 		<script>
 			var wpApiSettings = {
-				'root': 'http://localhost/wp-json/'
+				root: 'http://localhost/wp-json/',
+				nonce: 'not_a_real_nonce'
 			};
 		</script>
 		<script src="../../src/wp-includes/js/wp-util.js"></script>
@@ -77,6 +78,7 @@
 		<script src="../../src/wp-includes/js/customize-models.js"></script>
 		<script src="../../src/wp-includes/js/shortcode.js"></script>
 		<script src="../../src/wp-admin/js/customize-controls.js"></script>
+		<script src="../../src/wp-includes/js/api-request.js"></script>
 		<script src="../../src/wp-includes/js/wp-api.js"></script>
 
 		<script type='text/javascript' src='../../src/wp-includes/js/jquery/ui/core.js'></script>
@@ -122,6 +124,7 @@
 		<script src="wp-admin/js/customize-base.js"></script>
 		<script src="wp-admin/js/customize-header.js"></script>
 		<script src="wp-includes/js/shortcode.js"></script>
+		<script src="wp-includes/js/api-request.js"></script>
 		<script src="wp-includes/js/wp-api.js"></script>
 		<script src="wp-admin/js/customize-controls.js"></script>
 		<script src="wp-admin/js/customize-controls-utils.js"></script>
diff --git a/tests/qunit/wp-includes/js/api-request.js b/tests/qunit/wp-includes/js/api-request.js
new file mode 100644
index 0000000..079ef49
--- /dev/null
+++ b/tests/qunit/wp-includes/js/api-request.js
@@ -0,0 +1,164 @@
+/* global wp, wpApiSettings */
+( function( QUnit ) {
+	var originalRootUrl = window.wpApiSettings.root;
+
+	var nonceHeader = { 'X-WP-Nonce': 'not_a_real_nonce' };
+
+	QUnit.module( 'wp-api-request', {
+		afterEach: function() {
+			window.wpApiSettings.root = originalRootUrl;
+		}
+	} );
+
+	QUnit.test( 'does not mutate original object', function( assert ) {
+		var settingsOriginal = {
+			url: 'aaaaa',
+			path: 'wp/v2/posts',
+			headers: {
+				'Header-Name': 'value'
+			},
+			data: {
+				orderby: 'something'
+			}
+		};
+
+		var settings = wp.apiRequest.buildAjaxOptions( settingsOriginal );
+
+		assert.notStrictEqual( settings, settingsOriginal );
+		assert.notStrictEqual( settings.headers, settingsOriginal.headers );
+		assert.strictEqual( settings.data, settingsOriginal.data );
+
+		assert.deepEqual( settings, {
+			url: 'http://localhost/wp-json/wp/v2/posts',
+			headers: {
+				'X-WP-Nonce': 'not_a_real_nonce',
+				'Header-Name': 'value'
+			},
+			data: {
+				orderby: 'something'
+			}
+		} );
+
+		assert.deepEqual( settingsOriginal, {
+			url: 'aaaaa',
+			path: 'wp/v2/posts',
+			headers: {
+				'Header-Name': 'value'
+			},
+			data: {
+				orderby: 'something'
+			}
+		} );
+	} );
+
+	QUnit.test( 'does not add nonce header if already present', function( assert ) {
+		[ 'X-WP-Nonce', 'x-wp-nonce', 'X-WP-NONCE' ].forEach( function( headerName ) {
+			var nonceHeader = {};
+			nonceHeader[ headerName ] = 'still_not_a_real_nonce';
+
+			var settingsOriginal = {
+				url: 'aaaa',
+				headers: JSON.parse( JSON.stringify( nonceHeader ) )
+			};
+
+			var settings = wp.apiRequest.buildAjaxOptions( settingsOriginal );
+
+			assert.notStrictEqual( settings, settingsOriginal );
+			assert.strictEqual( settings.headers, settingsOriginal.headers );
+
+			assert.deepEqual( settings, {
+				url: 'aaaa',
+				headers: nonceHeader
+			} );
+		} );
+	} );
+
+	QUnit.test( 'does not add nonce header if ?_wpnonce=... present', function( assert ) {
+		var settingsOriginal = {
+			url: 'aaaa',
+			data: {
+				_wpnonce: 'definitely_not_a_real_nonce'
+			}
+		};
+
+		var settings = wp.apiRequest.buildAjaxOptions( settingsOriginal );
+
+		assert.notStrictEqual( settings, settingsOriginal );
+
+		assert.deepEqual( settings, {
+			url: 'aaaa',
+			headers: {},
+			data: {
+				_wpnonce: 'definitely_not_a_real_nonce'
+			}
+		} );
+	} );
+
+	QUnit.test( 'accepts namespace and endpoint', function( assert ) {
+		assert.deepEqual( wp.apiRequest.buildAjaxOptions( {
+			namespace: 'wp/v2',
+			endpoint: 'posts'
+		} ), {
+			url: 'http://localhost/wp-json/wp/v2/posts',
+			headers: nonceHeader
+		} );
+	} );
+
+	QUnit.test( 'accepts namespace and endpoint with slashes', function( assert ) {
+		assert.deepEqual( wp.apiRequest.buildAjaxOptions( {
+			namespace: '/wp/v2/',
+			endpoint: '/posts'
+		} ), {
+			url: 'http://localhost/wp-json/wp/v2/posts',
+			headers: nonceHeader
+		} );
+	} );
+
+	QUnit.test( 'accepts namespace and empty endpoint', function( assert ) {
+		assert.deepEqual( wp.apiRequest.buildAjaxOptions( {
+			namespace: 'wp/v2',
+			endpoint: ''
+		} ), {
+			url: 'http://localhost/wp-json/wp/v2',
+			headers: nonceHeader
+		} );
+	} );
+
+	QUnit.test( 'accepts empty namespace and empty endpoint', function( assert ) {
+		assert.deepEqual( wp.apiRequest.buildAjaxOptions( {
+			namespace: '',
+			endpoint: ''
+		} ), {
+			url: 'http://localhost/wp-json/',
+			headers: nonceHeader
+		} );
+	} );
+
+	QUnit.test(
+		'accepts namespace and endpoint with slashes (plain permalinks)',
+		function( assert ) {
+			window.wpApiSettings.root = 'http://localhost/?rest_route=/';
+			assert.deepEqual( wp.apiRequest.buildAjaxOptions( {
+				namespace: '/wp/v2/',
+				endpoint: '/posts'
+			} ), {
+				url: 'http://localhost/?rest_route=/wp/v2/posts',
+				headers: nonceHeader
+			} );
+		}
+	);
+
+	QUnit.test(
+		'accepts empty namespace and empty endpoint (plain permalinks)',
+		function( assert ) {
+			window.wpApiSettings.root = 'http://localhost/?rest_route=/';
+			assert.deepEqual( wp.apiRequest.buildAjaxOptions( {
+				namespace: '',
+				endpoint: ''
+			} ), {
+				url: 'http://localhost/?rest_route=/',
+				headers: nonceHeader
+			} );
+		}
+	);
+} )( window.QUnit );
