diff --git a/garnet/lib/rust/omaha_client/BUILD.gn b/garnet/lib/rust/omaha_client/BUILD.gn
index 769050aa90d2e977a5ef3acffcaa72825fb24801..dbadfe6b44e9b83eef4e70fa24e33463eaf38090 100644
--- a/garnet/lib/rust/omaha_client/BUILD.gn
+++ b/garnet/lib/rust/omaha_client/BUILD.gn
@@ -16,6 +16,7 @@ rustc_library("omaha_client") {
     "//garnet/public/rust/fuchsia-hyper",
     "//third_party/rust_crates:failure",
     "//third_party/rust_crates:futures-preview",
+    "//third_party/rust_crates:http",
     "//third_party/rust_crates:hyper",
     "//third_party/rust_crates:hyper-rustls",
     "//third_party/rust_crates:log",
diff --git a/garnet/lib/rust/omaha_client/src/configuration/mod.rs b/garnet/lib/rust/omaha_client/src/configuration/mod.rs
index ff6ebacc0bc5959cf9e63b579323b6258ad83649..edef0cd5fdaa22f66f5e9d0a26929612bc664628 100644
--- a/garnet/lib/rust/omaha_client/src/configuration/mod.rs
+++ b/garnet/lib/rust/omaha_client/src/configuration/mod.rs
@@ -24,4 +24,28 @@ pub struct Config {
     pub updater: Updater,
 
     pub os: OS,
+
+    /// This is the address of the Omaha service that should be used.
+    pub service_url: String,
+}
+
+#[cfg(test)]
+pub mod test_support {
+
+    use super::*;
+    use crate::{common::Version, protocol::request::OS};
+
+    /// Handy generator for an updater configuration.  Used to reduce test boilerplate.
+    pub fn config_generator() -> Config {
+        Config {
+            updater: Updater { name: "updater".to_string(), version: Version([1, 2, 3, 4]) },
+            os: OS {
+                platform: "platform".to_string(),
+                version: "0.1.2.3".to_string(),
+                service_pack: "sp".to_string(),
+                arch: "test_arch".to_string(),
+            },
+            service_url: "http://example.com/".to_string(),
+        }
+    }
 }
diff --git a/garnet/lib/rust/omaha_client/src/protocol/request/mod.rs b/garnet/lib/rust/omaha_client/src/protocol/request/mod.rs
index c57de600c7a53c43ac554673e45e6b93d43fc447..ec42ebf70e3a937ebfafa8f73b43e0095303ebfb 100644
--- a/garnet/lib/rust/omaha_client/src/protocol/request/mod.rs
+++ b/garnet/lib/rust/omaha_client/src/protocol/request/mod.rs
@@ -9,6 +9,18 @@ use serde_repr::Serialize_repr;
 #[cfg(test)]
 mod tests;
 
+/// This is the key for the http request header that identifies the 'updater' that is sending a
+/// request.
+pub const HEADER_UPDATER_NAME: &str = "X-Goog-Update-Updater";
+
+/// This is the key for the http request header that identifies whether this is an interactive
+/// or a background update (see InstallSource).
+pub const HEADER_INTERACTIVITY: &str = "X-Goog-Update-Interactivity";
+
+/// This is the key for the http request header that identifies the app id(s) that are included in
+/// this request.
+pub const HEADER_APP_ID: &str = "X-Goog-Update-AppId";
+
 /// An Omaha protocol request.
 ///
 /// This holds the data for constructing a request to the Omaha service.
@@ -61,7 +73,7 @@ pub struct Request {
 /// wrapping that Omaha expects to see.
 #[derive(Debug, Default, Serialize)]
 pub struct RequestWrapper {
-    request: Request,
+    pub request: Request,
 }
 
 /// Enum of the possible reasons that this update request was initiated.
diff --git a/garnet/lib/rust/omaha_client/src/requests/mod.rs b/garnet/lib/rust/omaha_client/src/requests/mod.rs
index e3aeb70082e4ea1cbaa0ecd27659903df71fb0db..4e729e71c9e5c46a8f249ccdd938e5b3a5264273 100644
--- a/garnet/lib/rust/omaha_client/src/requests/mod.rs
+++ b/garnet/lib/rust/omaha_client/src/requests/mod.rs
@@ -6,15 +6,43 @@ use crate::{
     common::App,
     configuration::Config,
     protocol::{
-        request::{Event, InstallSource, Ping, Request, UpdateCheck},
+        request::{
+            Event, InstallSource, Ping, Request, RequestWrapper, UpdateCheck, HEADER_APP_ID,
+            HEADER_INTERACTIVITY, HEADER_UPDATER_NAME,
+        },
         Cohort, PROTOCOL_V3,
     },
 };
 
+use http;
 use log::*;
+use std::result;
 
 type ProtocolApp = crate::protocol::request::App;
 
+/// Building a request can fail for multiple reasons, this enum consolidates them into a single
+/// type that can be used to express those reasons.
+#[derive(Debug)]
+pub enum Error {
+    Json(serde_json::Error),
+    Http(http::Error),
+}
+
+impl From<serde_json::Error> for Error {
+    fn from(e: serde_json::Error) -> Self {
+        Error::Json(e)
+    }
+}
+
+impl From<http::Error> for Error {
+    fn from(e: http::Error) -> Self {
+        Error::Http(e)
+    }
+}
+
+/// The builder's own Result type.
+pub type Result<T> = result::Result<T, Error>;
+
 /// These are the parameters that describe how the request should be performed.
 #[derive(Clone, Debug, PartialEq)]
 pub struct RequestParams {
@@ -177,19 +205,80 @@ impl<'a> RequestBuilder<'a> {
     /// This function constructs the protocol::request::Request object from this Builder.
     ///
     /// Note that the builder is consumed in the process, and cannot be used afterward.
-    pub fn build(self) -> Request {
-        let protocol_apps =
-            self.app_entries.into_iter().map(|entry| ProtocolApp::from(entry)).collect();
-
-        Request {
-            protocol_version: PROTOCOL_V3.to_string(),
-            updater: self.config.updater.name.clone(),
-            updater_version: self.config.updater.version.to_string(),
-            install_source: self.params.source.clone(),
-            is_machine: true,
-            os: self.config.os.clone(),
-            apps: protocol_apps,
+    pub fn build(self) -> Result<http::Request<hyper::Body>> {
+        self.build_intermediate().into()
+    }
+
+    /// Helper function that constructs the request body from the builder.
+    fn build_intermediate(self) -> Intermediate {
+        let mut headers = vec![
+            // Set the content-type to be JSON.
+            (http::header::CONTENT_TYPE.as_str(), "application/json".to_string()),
+            // The updater name header is always set directly from the name in the configuration
+            (HEADER_UPDATER_NAME, self.config.updater.name.clone()),
+            // The interactivity header is set based on the source of the request that's set in
+            // the request params
+            (
+                HEADER_INTERACTIVITY,
+                match self.params.source {
+                    InstallSource::OnDemand => "fg".to_string(),
+                    InstallSource::ScheduledTask => "bg".to_string(),
+                },
+            ),
+        ];
+        // And the app id header is based on the first app id in the request.
+        // TODO: Send all app ids, or only send the first based on configuration.
+        if let Some(main_app) = self.app_entries.first() {
+            headers.push((HEADER_APP_ID, main_app.app.id.clone()));
         }
+
+        let apps = self.app_entries.into_iter().map(|entry| ProtocolApp::from(entry)).collect();
+
+        Intermediate {
+            uri: self.config.service_url.clone(),
+            headers,
+            body: RequestWrapper {
+                request: Request {
+                    protocol_version: PROTOCOL_V3.to_string(),
+                    updater: self.config.updater.name.clone(),
+                    updater_version: self.config.updater.version.to_string(),
+                    install_source: self.params.source.clone(),
+                    is_machine: true,
+                    os: self.config.os.clone(),
+                    apps,
+                },
+            },
+        }
+    }
+}
+
+/// As the name implies, this is an itermediate that can be used to construct an http::Request from
+/// the data that's in the Builder.  It allows for type-aware inspection of the constructed protcol
+/// request, as well as the full construction of the http request (uri, headers, body).
+///
+/// This struct owns all of it's data, so that they can be moved directly into the constructed http
+/// request.
+struct Intermediate {
+    /// The URI for the http request.
+    uri: String,
+
+    /// The http request headers, in key:&str=value:String pairs
+    headers: Vec<(&'static str, String)>,
+
+    /// The request body, still in object form as a RequestWrapper
+    body: RequestWrapper,
+}
+
+impl From<Intermediate> for Result<http::Request<hyper::Body>> {
+    fn from(intermediate: Intermediate) -> Self {
+        let mut builder = hyper::Request::get(intermediate.uri);
+        for (key, value) in intermediate.headers {
+            builder.header(key, value);
+        }
+
+        let body = serde_json::to_string(&intermediate.body)?;
+        let request = builder.body(body.into())?;
+        Ok(request)
     }
 }
 
@@ -198,23 +287,12 @@ mod tests {
     use super::*;
     use crate::{
         common::Version,
-        configuration::Updater,
-        protocol::request::{EventResult, EventType, OS},
+        configuration::test_support::config_generator,
+        protocol::request::{EventResult, EventType},
     };
+    use futures::{compat::Stream01CompatExt, executor::block_on, prelude::*};
     use pretty_assertions::assert_eq;
-
-    /// Handy generator for an updater configuration.  Used to reduce test boilerplate.
-    fn config_generator() -> Config {
-        Config {
-            updater: Updater { name: "updater".to_string(), version: Version([1, 2, 3, 4]) },
-            os: OS {
-                platform: "platform".to_string(),
-                version: "0.1.2.3".to_string(),
-                service_pack: "sp".to_string(),
-                arch: "test_arch".to_string(),
-            },
-        }
-    }
+    use serde_json::json;
 
     /// Test that a simple request's fields are all correct:
     ///
@@ -224,7 +302,7 @@ mod tests {
     pub fn test_simple_request() {
         let config = config_generator();
 
-        let request = RequestBuilder::new(
+        let intermediate = RequestBuilder::new(
             &config,
             &RequestParams { source: InstallSource::OnDemand, use_configured_proxies: false },
         )
@@ -232,9 +310,10 @@ mod tests {
             &App { id: "app id".to_string(), version: Version([5, 6, 7, 8]), fingerprint: None },
             &Some(Cohort::new("some-channel")),
         )
-        .build();
+        .build_intermediate();
 
         // Assert that all the request fields are accurate (this is in their order of declaration)
+        let request = intermediate.body.request;
         assert_eq!(request.protocol_version, "3.0");
         assert_eq!(request.updater, config.updater.name);
         assert_eq!(request.updater_version, config.updater.version.to_string());
@@ -255,6 +334,85 @@ mod tests {
         assert_eq!(app.update_check, Some(UpdateCheck::default()));
         assert!(app.events.is_empty());
         assert_eq!(app.ping, None);
+
+        // Assert that the headers are set correctly
+        let headers = intermediate.headers;
+        assert_eq!(4, headers.len());
+        assert!(headers.contains(&("content-type", "application/json".to_string())));
+        assert!(headers.contains(&(HEADER_UPDATER_NAME, config.updater.name)));
+        assert!(headers.contains(&(HEADER_APP_ID, "app id".to_string())));
+        assert!(headers.contains(&(HEADER_INTERACTIVITY, "fg".to_string())));
+    }
+
+    /// Test that a simple update check results in the correct HTTP request:
+    ///  - service url
+    ///  - headers
+    ///  - request body
+    #[test]
+    pub fn test_single_request() {
+        let config = config_generator();
+
+        let (parts, body) = RequestBuilder::new(
+            &config,
+            &RequestParams { source: InstallSource::OnDemand, use_configured_proxies: false },
+        )
+        .add_update_check(
+            &App { id: "app id".to_string(), version: Version([5, 6, 7, 8]), fingerprint: None },
+            &Some(Cohort::new("some-channel")),
+        )
+        .build()
+        .unwrap()
+        .into_parts();
+
+        // Assert that the HTTP method and uri are accurate
+        assert_eq!(http::Method::GET, parts.method);
+        assert_eq!(config.service_url, parts.uri.to_string());
+
+        // Assert that all the request body is correct, by generating an equivalent JSON one and
+        // then comparing the resultant byte bodies
+        let expected = json!({
+            "request": {
+                "protocol": "3.0",
+                "updater": config.updater.name,
+                "updaterversion": config.updater.version.to_string(),
+                "installsource": "ondemand",
+                "ismachine": true,
+                "os": {
+                    "platform": config.os.platform,
+                    "version": config.os.version,
+                    "sp": config.os.service_pack,
+                    "arch": config.os.arch,
+                },
+                "app": [
+                    {
+                        "appid": "app id",
+                        "cohort": "some-channel",
+                        "version": "5.6.7.8",
+                        "updatecheck": {},
+                    },
+                ],
+            }
+        });
+
+        // Extract the request body out into a concatenated stream of Chunks, into a slice, so
+        // that serde can be used to parse the body into a JSON Value object that can be compared
+        // with the expected json constructed above.
+        let actual: serde_json::Value =
+            serde_json::from_slice(&block_on(body.compat().try_concat()).unwrap().to_vec())
+                .unwrap();
+
+        assert_eq!(expected, actual);
+
+        // Assert that the headers are all correct
+        let headers = parts.headers;
+        assert_eq!(4, headers.len());
+        assert_eq!("application/json", headers.get("content-type").unwrap().to_str().unwrap());
+        assert_eq!(
+            config.updater.name,
+            headers.get(HEADER_UPDATER_NAME).unwrap().to_str().unwrap()
+        );
+        assert_eq!("app id", headers.get(HEADER_APP_ID).unwrap().to_str().unwrap());
+        assert_eq!("fg", headers.get(HEADER_INTERACTIVITY).unwrap().to_str().unwrap());
     }
 
     /// Test that a ping is correctly added to an App entry.
@@ -262,7 +420,7 @@ mod tests {
     pub fn test_simple_ping() {
         let config = config_generator();
 
-        let request = RequestBuilder::new(
+        let intermediate = RequestBuilder::new(
             &config,
             &RequestParams { source: InstallSource::ScheduledTask, use_configured_proxies: false },
         )
@@ -275,10 +433,10 @@ mod tests {
             &Some(Cohort::new("ping-channel")),
             &Ping { date_last_active: Some(34), date_last_roll_call: Some(45) },
         )
-        .build();
+        .build_intermediate();
 
         // Validate that the App was added, with it's cohort
-        let app = &request.apps[0];
+        let app = &intermediate.body.request.apps[0];
         assert_eq!(app.id, "ping app id");
         assert_eq!(app.version, "6.7.8.9");
         assert_eq!(app.cohort, Some(Cohort::new("ping-channel")));
@@ -288,6 +446,14 @@ mod tests {
         let ping = app.ping.as_ref().unwrap();
         assert_eq!(ping.date_last_active, Some(34));
         assert_eq!(ping.date_last_roll_call, Some(45));
+
+        // Assert that the headers are set correctly
+        let headers = intermediate.headers;
+        assert_eq!(4, headers.len());
+        assert!(headers.contains(&("content-type", "application/json".to_string())));
+        assert!(headers.contains(&(HEADER_UPDATER_NAME, config.updater.name)));
+        assert!(headers.contains(&(HEADER_APP_ID, "ping app id".to_string())));
+        assert!(headers.contains(&(HEADER_INTERACTIVITY, "bg".to_string())));
     }
 
     /// Test that an event is properly added to an App entry
@@ -313,7 +479,9 @@ mod tests {
                 ..Event::default()
             },
         )
-        .build();
+        .build_intermediate()
+        .body
+        .request;
 
         let app = &request.apps[0];
         assert_eq!(app.id, "event app id");
@@ -364,7 +532,9 @@ mod tests {
                 ..Event::default()
             },
         )
-        .build();
+        .build_intermediate()
+        .body
+        .request;
 
         // Validate that the resultant Request has the right fields and events
 
@@ -422,11 +592,13 @@ mod tests {
             &app_1_cohort,
             &Ping { date_last_active: Some(34), date_last_roll_call: Some(45) },
         )
-        .build();
+        .build_intermediate()
+        .body
+        .request;
 
         // Validate the resultant Request is correct.
 
-        // There should only be the two entries.
+        // There should only be the two app entries.
         assert_eq!(request.apps.len(), 2);
 
         // The first app should have the ping attached to it.
@@ -481,7 +653,7 @@ mod tests {
             &Ping { date_last_active: Some(34), date_last_roll_call: Some(45) },
         );
 
-        let request = builder.build();
+        let request = builder.build_intermediate().body.request;
 
         // Validate that the resultant request is correct.
 
@@ -544,7 +716,9 @@ mod tests {
                 ..Event::default()
             },
         )
-        .build();
+        .build_intermediate()
+        .body
+        .request;
 
         // There should only be the two entries.
         assert_eq!(request.apps.len(), 2);
@@ -607,7 +781,7 @@ mod tests {
             },
         );
 
-        let request = builder.build();
+        let request = builder.build_intermediate().body.request;
 
         // There should only be the two entries.
         assert_eq!(request.apps.len(), 2);