diff --git a/.eslintrc-test-paths.js b/.eslintrc-test-paths.js
index 9b7ce23e769de47f021789464ebf521a8b8027aa..9806aa4420ad09a6db0ff67314664d064152726c 100644
--- a/.eslintrc-test-paths.js
+++ b/.eslintrc-test-paths.js
@@ -165,6 +165,7 @@ const extraBrowserTestPaths = [
   "devtools/client/styleeditor/test/",
   "devtools/shared/commands/inspected-window/tests/",
   "devtools/shared/commands/inspector/tests/",
+  "devtools/shared/commands/network/tests/",
   "devtools/shared/commands/resource/tests/",
   "devtools/shared/commands/script/tests/",
   "devtools/shared/commands/target-configuration/tests/",
diff --git a/devtools/client/netmonitor/src/actions/http-custom-request.js b/devtools/client/netmonitor/src/actions/http-custom-request.js
index 163aab6e1cb663f574859537ae6b2f893ae53515..e045107410733a1f156ce7222673bfae1367eb37 100644
--- a/devtools/client/netmonitor/src/actions/http-custom-request.js
+++ b/devtools/client/netmonitor/src/actions/http-custom-request.js
@@ -69,8 +69,8 @@ function toggleHTTPCustomRequestPanel() {
 /**
  * Send a new HTTP request using the data in the custom request form.
  */
-function sendHTTPCustomRequest(connector, request) {
-  return async ({ dispatch, getState }) => {
+function sendHTTPCustomRequest(request) {
+  return async ({ dispatch, getState, connector, commands }) => {
     if (!request) {
       return;
     }
@@ -104,7 +104,7 @@ function sendHTTPCustomRequest(connector, request) {
       data.body = request.requestPostData.postData?.text;
     }
 
-    const { channelId } = await connector.sendHTTPRequest(data);
+    const { channelId } = await commands.networkCommand.sendHTTPRequest(data);
 
     const newRequest = getRequestByChannelId(getState(), channelId);
     // If the new custom request is available already select the request, else
diff --git a/devtools/client/netmonitor/src/actions/requests.js b/devtools/client/netmonitor/src/actions/requests.js
index f229b7e2025d28ef980246ab5edb56ceac36d017..838f2509a99bd24d63952baa36445c08314f5f63 100644
--- a/devtools/client/netmonitor/src/actions/requests.js
+++ b/devtools/client/netmonitor/src/actions/requests.js
@@ -85,8 +85,8 @@ function cloneSelectedRequest() {
 /**
  * Send a new HTTP request using the data in the custom request form.
  */
-function sendCustomRequest(connector, requestId = null) {
-  return async ({ dispatch, getState }) => {
+function sendCustomRequest(requestId = null) {
+  return async ({ dispatch, getState, connector, commands }) => {
     let request;
     if (requestId) {
       request = getRequestById(getState(), requestId);
@@ -123,13 +123,11 @@ function sendCustomRequest(connector, requestId = null) {
       data.body = request.requestPostData.postData.text;
     }
 
-    // @backward-compat { version 85 } Introduced `channelId` to eventually
-    // replace `actor`.
-    const { channelId, actor } = await connector.sendHTTPRequest(data);
+    const { channelId } = await commands.networkCommand.sendHTTPRequest(data);
 
     dispatch({
       type: SEND_CUSTOM_REQUEST,
-      id: channelId || actor,
+      id: channelId,
     });
   };
 }
diff --git a/devtools/client/netmonitor/src/api.js b/devtools/client/netmonitor/src/api.js
index ed2a8e3914a2401c82fd8efa9660a555247e5594..8d760de788aa811c4d54b95fead8582bc24fb780 100644
--- a/devtools/client/netmonitor/src/api.js
+++ b/devtools/client/netmonitor/src/api.js
@@ -209,7 +209,7 @@ NetMonitorAPI.prototype = {
     this.store.dispatch(Actions.batchFlush());
     // Send custom request with same url, headers and body as the request
     // with the given requestId.
-    this.store.dispatch(Actions.sendCustomRequest(this.connector, requestId));
+    this.store.dispatch(Actions.sendCustomRequest(requestId));
   },
 };
 
diff --git a/devtools/client/netmonitor/src/components/CustomRequestPanel.js b/devtools/client/netmonitor/src/components/CustomRequestPanel.js
index e024bdf2d4c0a93cdebde09b31bd155cd78a19ff..922665fad2d48a0986c1073b7a968d3fb62a825b 100644
--- a/devtools/client/netmonitor/src/components/CustomRequestPanel.js
+++ b/devtools/client/netmonitor/src/components/CustomRequestPanel.js
@@ -369,8 +369,7 @@ module.exports = connect(
   (dispatch, props) => ({
     removeSelectedCustomRequest: () =>
       dispatch(Actions.removeSelectedCustomRequest()),
-    sendCustomRequest: () =>
-      dispatch(Actions.sendCustomRequest(props.connector)),
+    sendCustomRequest: () => dispatch(Actions.sendCustomRequest()),
     updateRequest: (id, data, batch) =>
       dispatch(Actions.updateRequest(id, data, batch)),
   })
diff --git a/devtools/client/netmonitor/src/components/new-request/HTTPCustomRequestPanel.js b/devtools/client/netmonitor/src/components/new-request/HTTPCustomRequestPanel.js
index 8a6854a18d10f4ca31b6fbc2ea28b2f602dcd579..a898fe38ebcca962f782fd7782ffac5868b471e0 100644
--- a/devtools/client/netmonitor/src/components/new-request/HTTPCustomRequestPanel.js
+++ b/devtools/client/netmonitor/src/components/new-request/HTTPCustomRequestPanel.js
@@ -507,6 +507,6 @@ module.exports = connect(
   state => ({ request: getClickedRequest(state) }),
   (dispatch, props) => ({
     sendCustomRequest: request =>
-      dispatch(Actions.sendHTTPCustomRequest(props.connector, request)),
+      dispatch(Actions.sendHTTPCustomRequest(request)),
   })
 )(HTTPCustomRequestPanel);
diff --git a/devtools/client/netmonitor/src/components/request-details/HeadersPanel.js b/devtools/client/netmonitor/src/components/request-details/HeadersPanel.js
index a4fdbed0b3bb3e1c56a3a73ba13e01e8ca6f1bf0..b8e28766848ce27e858b345848ccefa21a2278b5 100644
--- a/devtools/client/netmonitor/src/components/request-details/HeadersPanel.js
+++ b/devtools/client/netmonitor/src/components/request-details/HeadersPanel.js
@@ -845,7 +845,6 @@ module.exports = connect(
     openHTTPCustomRequestTab: () =>
       dispatch(Actions.openHTTPCustomRequest(true)),
     cloneRequest: id => dispatch(Actions.cloneRequest(id)),
-    sendCustomRequest: () =>
-      dispatch(Actions.sendCustomRequest(props.connector)),
+    sendCustomRequest: () => dispatch(Actions.sendCustomRequest()),
   })
 )(HeadersPanel);
diff --git a/devtools/client/netmonitor/src/components/request-list/RequestListContent.js b/devtools/client/netmonitor/src/components/request-list/RequestListContent.js
index 362f77295a4f8c3c6bb2528aea3d76e6720d98ca..7610886b69316fc2d0325a83831bc839916f8040 100644
--- a/devtools/client/netmonitor/src/components/request-list/RequestListContent.js
+++ b/devtools/client/netmonitor/src/components/request-list/RequestListContent.js
@@ -482,10 +482,9 @@ module.exports = connect(
       dispatch(Actions.openHTTPCustomRequest(true)),
     closeHTTPCustomRequestTab: () =>
       dispatch(Actions.openHTTPCustomRequest(false)),
-    sendCustomRequest: () =>
-      dispatch(Actions.sendCustomRequest(props.connector)),
+    sendCustomRequest: () => dispatch(Actions.sendCustomRequest()),
     sendHTTPCustomRequest: request =>
-      dispatch(Actions.sendHTTPCustomRequest(props.connector, request)),
+      dispatch(Actions.sendHTTPCustomRequest(request)),
     openStatistics: open =>
       dispatch(Actions.openStatistics(props.connector, open)),
     openRequestBlockingAndAddUrl: url =>
diff --git a/devtools/client/netmonitor/src/connector/index.js b/devtools/client/netmonitor/src/connector/index.js
index 17218e5ab1a01cb833aa504dd00cbe8d6f67bc21..22d17628af77ee3efc12ba79fed13229a851c6b3 100644
--- a/devtools/client/netmonitor/src/connector/index.js
+++ b/devtools/client/netmonitor/src/connector/index.js
@@ -37,7 +37,6 @@ class Connector {
     this.disconnect = this.disconnect.bind(this);
     this.willNavigate = this.willNavigate.bind(this);
     this.navigate = this.navigate.bind(this);
-    this.sendHTTPRequest = this.sendHTTPRequest.bind(this);
     this.triggerActivity = this.triggerActivity.bind(this);
     this.viewSourceInDebugger = this.viewSourceInDebugger.bind(this);
     this.requestData = this.requestData.bind(this);
@@ -76,6 +75,7 @@ class Connector {
     this.getState = getState;
     this.toolbox = connection.toolbox;
     this.commands = this.toolbox.commands;
+    this.networkCommand = this.commands.networkCommand;
 
     // The owner object (NetMonitorAPI) received all events.
     this.owner = connection.owner;
@@ -344,19 +344,6 @@ class Connector {
     this.emitForTests(TEST_EVENTS.TIMELINE_EVENT, resource);
   }
 
-  /**
-   * Send a HTTP request data payload
-   *
-   * @param {object} data data payload would like to sent to backend
-   */
-  async sendHTTPRequest(data) {
-    const networkContentFront = await this.currentTarget.getFront(
-      "networkContent"
-    );
-    const { channelId } = await networkContentFront.sendHTTPRequest(data);
-    return { channelId };
-  }
-
   /*
    * Get the list of blocked URLs
    */
diff --git a/devtools/client/netmonitor/test/browser_net_new_request_panel_send_request.js b/devtools/client/netmonitor/test/browser_net_new_request_panel_send_request.js
index 677d02d45e3088e31894fdbe148af05da2048c25..9337c7cb94634a217b8ae6264a680d9a2bcc4bfb 100644
--- a/devtools/client/netmonitor/test/browser_net_new_request_panel_send_request.js
+++ b/devtools/client/netmonitor/test/browser_net_new_request_panel_send_request.js
@@ -20,8 +20,6 @@ add_task(async function() {
   info("Starting test... ");
 
   const { document, store, windowRequire, connector } = monitor.panelWin;
-  const { sendHTTPRequest } = connector;
-
   // Action should be processed synchronously in tests.
   const Actions = windowRequire("devtools/client/netmonitor/src/actions/index");
   store.dispatch(Actions.batchEnable(false));
@@ -50,7 +48,7 @@ add_task(async function() {
     },
   };
   const waitUntilRequestDisplayed = waitForNetworkEvents(monitor, 1);
-  sendHTTPRequest(request);
+  connector.networkCommand.sendHTTPRequest(request);
   await waitUntilRequestDisplayed;
 
   info("selecting first request");
diff --git a/devtools/client/netmonitor/test/browser_net_resend.js b/devtools/client/netmonitor/test/browser_net_resend.js
index 20d09e526f76ad9849eae53877657ece81574504..43c7c15ee8dba345a86dde5252dca325c6425147 100644
--- a/devtools/client/netmonitor/test/browser_net_resend.js
+++ b/devtools/client/netmonitor/test/browser_net_resend.js
@@ -175,7 +175,7 @@ async function testOldEditAndResendPanel() {
   });
   info("Starting test... ");
 
-  const { document, store, windowRequire, connector } = monitor.panelWin;
+  const { document, store, windowRequire } = monitor.panelWin;
   const Actions = windowRequire("devtools/client/netmonitor/src/actions/index");
   const { getSelectedRequest, getSortedRequests } = windowRequire(
     "devtools/client/netmonitor/src/selectors/index"
@@ -214,7 +214,7 @@ async function testOldEditAndResendPanel() {
 
   // send the new request
   const wait = waitForNetworkEvents(monitor, 1);
-  store.dispatch(Actions.sendCustomRequest(connector));
+  store.dispatch(Actions.sendCustomRequest());
   await wait;
 
   let sentItem;
diff --git a/devtools/client/netmonitor/test/browser_net_resend_cors.js b/devtools/client/netmonitor/test/browser_net_resend_cors.js
index ff1d6fc2818bd252113989bc25ba2c5d9973a3f7..031d55f8e4c2182397f8c1d9d04828da1f59c4c1 100644
--- a/devtools/client/netmonitor/test/browser_net_resend_cors.js
+++ b/devtools/client/netmonitor/test/browser_net_resend_cors.js
@@ -70,7 +70,7 @@ add_task(async function() {
     store.dispatch(Actions.cloneRequest(item.id));
 
     info("Sending the cloned request (without change)");
-    store.dispatch(Actions.sendCustomRequest(connector, item.id));
+    store.dispatch(Actions.sendCustomRequest(item.id));
 
     await waitUntil(
       () => getSortedRequests(store.getState()).length === length + 1
diff --git a/devtools/client/netmonitor/test/browser_net_resend_headers.js b/devtools/client/netmonitor/test/browser_net_resend_headers.js
index 34b35f5c2540c24a2e6277f2543861f8c384e861..d3b65bb7e3430ccaf72c154b209b77e8e2b0b6fe 100644
--- a/devtools/client/netmonitor/test/browser_net_resend_headers.js
+++ b/devtools/client/netmonitor/test/browser_net_resend_headers.js
@@ -15,7 +15,7 @@ add_task(async function() {
 
   const { store, windowRequire, connector } = monitor.panelWin;
   const Actions = windowRequire("devtools/client/netmonitor/src/actions/index");
-  const { requestData, sendHTTPRequest } = connector;
+  const { requestData } = connector;
   const { getSortedRequests } = windowRequire(
     "devtools/client/netmonitor/src/selectors/index"
   );
@@ -33,7 +33,7 @@ add_task(async function() {
   ];
 
   const wait = waitForNetworkEvents(monitor, 1);
-  sendHTTPRequest({
+  connector.networkCommand.sendHTTPRequest({
     url: requestUrl,
     method: "POST",
     headers: requestHeaders,
diff --git a/devtools/client/netmonitor/test/browser_net_resend_hidden_headers.js b/devtools/client/netmonitor/test/browser_net_resend_hidden_headers.js
index 138a97b51d88c85939c3ae13058e47ce9f6ef1fb..cbf7c390e3a54ae742313af68e2e8c49dc487cd9 100644
--- a/devtools/client/netmonitor/test/browser_net_resend_hidden_headers.js
+++ b/devtools/client/netmonitor/test/browser_net_resend_hidden_headers.js
@@ -15,7 +15,6 @@ add_task(async function() {
 
   const { store, windowRequire, connector } = monitor.panelWin;
   const Actions = windowRequire("devtools/client/netmonitor/src/actions/index");
-  const { sendHTTPRequest } = connector;
 
   const { getSortedRequests } = windowRequire(
     "devtools/client/netmonitor/src/selectors/index"
@@ -29,7 +28,7 @@ add_task(async function() {
   ];
 
   const originalRequest = waitForNetworkEvents(monitor, 1);
-  sendHTTPRequest({
+  connector.networkCommand.sendHTTPRequest({
     url: requestUrl,
     method: "GET",
     headers: requestHeaders,
@@ -49,7 +48,7 @@ add_task(async function() {
 
   const clonedRequest = waitForNetworkEvents(monitor, 1);
 
-  store.dispatch(Actions.sendCustomRequest(connector, originalItem.id));
+  store.dispatch(Actions.sendCustomRequest(originalItem.id));
 
   await clonedRequest;
 
diff --git a/devtools/client/webconsole/components/Output/message-types/NetworkEventMessage.js b/devtools/client/webconsole/components/Output/message-types/NetworkEventMessage.js
index 24232610c6a1989281bff760cd543b2253e32d84..6ca977ab897adff0dc313334c36a721d4c46fa8c 100644
--- a/devtools/client/webconsole/components/Output/message-types/NetworkEventMessage.js
+++ b/devtools/client/webconsole/components/Output/message-types/NetworkEventMessage.js
@@ -179,7 +179,6 @@ function NetworkEventMessage({
     getLongString: grip => {
       return serviceContainer.getLongString(grip);
     },
-    sendHTTPRequest: () => {},
     triggerActivity: () => {},
     requestData: (requestId, dataType) => {
       return serviceContainer.requestData(requestId, dataType);
diff --git a/devtools/shared/commands/index.js b/devtools/shared/commands/index.js
index 01bd52895a7b02ac162d005dd0b8f08b5aaa2524..94cf7717cb83144f4dd4c3d27a8e853c1aa94a17 100644
--- a/devtools/shared/commands/index.js
+++ b/devtools/shared/commands/index.js
@@ -12,6 +12,7 @@ const Commands = {
   inspectedWindowCommand:
     "devtools/shared/commands/inspected-window/inspected-window-command",
   inspectorCommand: "devtools/shared/commands/inspector/inspector-command",
+  networkCommand: "devtools/shared/commands/network/network-command",
   resourceCommand: "devtools/shared/commands/resource/resource-command",
   rootResourceCommand:
     "devtools/shared/commands/root-resource/root-resource-command",
diff --git a/devtools/shared/commands/moz.build b/devtools/shared/commands/moz.build
index 36bacf281d58375e6cfbfd32b11466b6d7ed6ba3..bcd8a148108168eb59f95c9859b226146455ca3a 100644
--- a/devtools/shared/commands/moz.build
+++ b/devtools/shared/commands/moz.build
@@ -5,6 +5,7 @@
 DIRS += [
     "inspected-window",
     "inspector",
+    "network",
     "resource",
     "root-resource",
     "script",
diff --git a/devtools/shared/commands/network/moz.build b/devtools/shared/commands/network/moz.build
new file mode 100644
index 0000000000000000000000000000000000000000..e765e5ac76d56f7a1a71e1fa74055fbed54ee63e
--- /dev/null
+++ b/devtools/shared/commands/network/moz.build
@@ -0,0 +1,10 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+DevToolsModules(
+    "network-command.js",
+)
+
+if CONFIG["MOZ_BUILD_APP"] != "mobile/android":
+    BROWSER_CHROME_MANIFESTS += ["tests/browser.ini"]
diff --git a/devtools/shared/commands/network/network-command.js b/devtools/shared/commands/network/network-command.js
new file mode 100644
index 0000000000000000000000000000000000000000..0bc04aee29a9aa2a94ecce0fc226674558cd3d1c
--- /dev/null
+++ b/devtools/shared/commands/network/network-command.js
@@ -0,0 +1,42 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+class NetworkCommand {
+  /**
+   * This class helps listen, inspect and control network requests.
+   *
+   * @param {DescriptorFront} descriptorFront
+   *        The context to inspect identified by this descriptor.
+   * @param {WatcherFront} watcherFront
+   *        If available, a reference to the related Watcher Front.
+   * @param {Object} commands
+   *        The commands object with all interfaces defined from devtools/shared/commands/
+   */
+  constructor({ descriptorFront, watcherFront, commands }) {
+    this.commands = commands;
+    this.descriptorFront = descriptorFront;
+    this.watcherFront = watcherFront;
+  }
+
+  /**
+   * Send a HTTP request data payload
+   *
+   * @param {object} data data payload would like to sent to backend
+   */
+  async sendHTTPRequest(data) {
+    // By default use the top-level target, but we might at some point
+    // allow using another target.
+    const networkContentFront = await this.commands.targetCommand.targetFront.getFront(
+      "networkContent"
+    );
+    const { channelId } = await networkContentFront.sendHTTPRequest(data);
+    return { channelId };
+  }
+
+  destroy() {}
+}
+
+module.exports = NetworkCommand;
diff --git a/devtools/shared/commands/network/tests/browser.ini b/devtools/shared/commands/network/tests/browser.ini
new file mode 100644
index 0000000000000000000000000000000000000000..32ebfef8b3ba38c63c23011e60cfdca05b854007
--- /dev/null
+++ b/devtools/shared/commands/network/tests/browser.ini
@@ -0,0 +1,10 @@
+[DEFAULT]
+tags = devtools
+subsuite = devtools
+support-files =
+  !/devtools/client/shared/test/shared-head.js
+  !/devtools/client/shared/test/telemetry-test-helpers.js
+  !/devtools/client/shared/test/highlighter-test-actor.js
+  head.js
+
+[browser_network_command_sendHTTPRequest.js]
diff --git a/devtools/shared/commands/network/tests/browser_network_command_sendHTTPRequest.js b/devtools/shared/commands/network/tests/browser_network_command_sendHTTPRequest.js
new file mode 100644
index 0000000000000000000000000000000000000000..9062b0de7870039845304056d1a92d26541c7683
--- /dev/null
+++ b/devtools/shared/commands/network/tests/browser_network_command_sendHTTPRequest.js
@@ -0,0 +1,78 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test the NetworkCommand's sendHTTPRequest
+
+add_task(async function() {
+  info("Test NetworkCommand.sendHTTPRequest");
+  const tab = await addTab("data:text/html,foo");
+  const commands = await CommandsFactory.forTab(tab);
+
+  // We have to ensure TargetCommand is initialized to have access to the top level target
+  // from NetworkCommand.sendHTTPRequest
+  await commands.targetCommand.startListening();
+
+  const { networkCommand } = commands;
+
+  const httpServer = createTestHTTPServer();
+  const onRequest = new Promise(resolve => {
+    httpServer.registerPathHandler(
+      "/http-request.html",
+      (request, response) => {
+        response.setStatusLine(request.httpVersion, 200, "OK");
+        response.write("Response body");
+        resolve(request);
+      }
+    );
+  });
+  const url = `http://localhost:${httpServer.identity.primaryPort}/http-request.html`;
+
+  info("Call NetworkCommand.sendHTTPRequest");
+  const { resourceCommand } = commands;
+  const { onResource } = await resourceCommand.waitForNextResource(
+    resourceCommand.TYPES.NETWORK_EVENT
+  );
+  const { channelId } = await networkCommand.sendHTTPRequest({
+    url,
+    method: "POST",
+    headers: [{ name: "Request", value: "Header" }],
+    body: "Hello",
+    cause: {
+      loadingDocumentUri: "https://example.com",
+      stacktraceAvailable: true,
+      type: "xhr",
+    },
+  });
+  ok(channelId, "Received a channel id in response");
+  const resource = await onResource;
+  is(
+    resource.resourceId,
+    channelId,
+    "NETWORK_EVENT resource channelId is the same as the one returned by sendHTTPRequest"
+  );
+
+  const request = await onRequest;
+  is(request.method, "POST", "Request method is correct");
+  is(request.getHeader("Request"), "Header", "The custom header was passed");
+  is(fetchRequestBody(request), "Hello", "The request POST's body is correct");
+
+  await commands.destroy();
+});
+
+const BinaryInputStream = Components.Constructor(
+  "@mozilla.org/binaryinputstream;1",
+  "nsIBinaryInputStream",
+  "setInputStream"
+);
+
+function fetchRequestBody(request) {
+  let body = "";
+  const bodyStream = new BinaryInputStream(request.bodyInputStream);
+  let avail = 0;
+  while ((avail = bodyStream.available()) > 0) {
+    body += String.fromCharCode.apply(String, bodyStream.readByteArray(avail));
+  }
+  return body;
+}
diff --git a/devtools/shared/commands/network/tests/head.js b/devtools/shared/commands/network/tests/head.js
new file mode 100644
index 0000000000000000000000000000000000000000..227e8ae9d967ded2216a857d86673a6ba5c9afaf
--- /dev/null
+++ b/devtools/shared/commands/network/tests/head.js
@@ -0,0 +1,13 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+/* eslint no-unused-vars: [2, {"vars": "local"}] */
+/* import-globals-from ../../../../client/shared/test/shared-head.js */
+
+Services.scriptloader.loadSubScript(
+  "chrome://mochitests/content/browser/devtools/client/shared/test/shared-head.js",
+  this
+);