diff --git a/lib/types/tunnelbroker/notif-types.js b/lib/types/tunnelbroker/notif-types.js
--- a/lib/types/tunnelbroker/notif-types.js
+++ b/lib/types/tunnelbroker/notif-types.js
@@ -23,7 +23,15 @@
   +payload: string,
 };
 
+export type TunnelbrokerWNSNotif = {
+  +type: 'WNSNotif',
+  +clientMessageID: string,
+  +deviceID: string,
+  +payload: string,
+};
+
 export type TunnelbrokerNotif =
   | TunnelbrokerAPNsNotif
   | TunnelbrokerFCMNotif
-  | TunnelbrokerWebPushNotif;
+  | TunnelbrokerWebPushNotif
+  | TunnelbrokerWNSNotif;
diff --git a/services/tunnelbroker/src/notifs/mod.rs b/services/tunnelbroker/src/notifs/mod.rs
--- a/services/tunnelbroker/src/notifs/mod.rs
+++ b/services/tunnelbroker/src/notifs/mod.rs
@@ -14,7 +14,7 @@
   APNs,
   FCM,
   WebPush,
-  WNs,
+  WNS,
 }
 
 impl NotifClientType {
@@ -25,7 +25,7 @@
       }
       NotifClientType::FCM => platform == Platform::Android,
       NotifClientType::WebPush => platform == Platform::Web,
-      NotifClientType::WNs => platform == Platform::Windows,
+      NotifClientType::WNS => platform == Platform::Windows,
     }
   }
 }
diff --git a/services/tunnelbroker/src/notifs/wns/error.rs b/services/tunnelbroker/src/notifs/wns/error.rs
--- a/services/tunnelbroker/src/notifs/wns/error.rs
+++ b/services/tunnelbroker/src/notifs/wns/error.rs
@@ -1,15 +1,29 @@
 use derive_more::{Display, Error, From};
 
+use super::response::WNSErrorResponse;
+
 #[derive(Debug, From, Display, Error)]
 pub enum Error {
   Reqwest(reqwest::Error),
   SerdeJson(serde_json::Error),
-  #[display(fmt = "Token not found in response")]
-  TokenNotFound,
-  #[display(fmt = "Expiry time not found in response")]
-  ExpiryNotFound,
+  #[display(fmt = "WNS Token Error: {}", _0)]
+  WNSToken(WNSTokenError),
   #[display(fmt = "Failed to acquire read lock")]
   ReadLock,
   #[display(fmt = "Failed to acquire write lock")]
   WriteLock,
+  #[display(fmt = "WNS Notification Error: {}", _0)]
+  WNSNotification(WNSErrorResponse),
+}
+
+#[derive(Debug, From, Display)]
+pub enum WNSTokenError {
+  #[display(fmt = "Token not found in response")]
+  TokenNotFound,
+  #[display(fmt = "Expiry time not found in response")]
+  ExpiryNotFound,
+  #[display(fmt = "Unknown Error: {}", _0)]
+  Unknown(String),
 }
+
+impl std::error::Error for WNSTokenError {}
diff --git a/services/tunnelbroker/src/notifs/wns/mod.rs b/services/tunnelbroker/src/notifs/wns/mod.rs
--- a/services/tunnelbroker/src/notifs/wns/mod.rs
+++ b/services/tunnelbroker/src/notifs/wns/mod.rs
@@ -1,4 +1,7 @@
 use crate::notifs::wns::config::WNSConfig;
+use error::WNSTokenError;
+use reqwest::StatusCode;
+use response::WNSErrorResponse;
 use std::{
   sync::{Arc, RwLock},
   time::{Duration, SystemTime},
@@ -6,6 +9,7 @@
 
 pub mod config;
 mod error;
+mod response;
 
 #[derive(Debug, Clone)]
 pub struct WNSAccessToken {
@@ -13,6 +17,12 @@
   expires: SystemTime,
 }
 
+#[derive(Debug, Clone)]
+pub struct WNSNotif {
+  pub device_token: String,
+  pub payload: String,
+}
+
 #[derive(Clone)]
 pub struct WNSClient {
   http_client: reqwest::Client,
@@ -30,9 +40,45 @@
     })
   }
 
-  pub async fn get_wns_token(
-    &mut self,
-  ) -> Result<Option<String>, error::Error> {
+  pub async fn send(&self, notif: WNSNotif) -> Result<(), error::Error> {
+    let wns_access_token = self.get_wns_token().await?;
+
+    let url = notif.device_token;
+
+    // Send the notification
+    let response = self
+      .http_client
+      .post(&url)
+      .header("Content-Type", "application/octet-stream")
+      .header("X-WNS-Type", "wns/raw")
+      .bearer_auth(wns_access_token)
+      .body(notif.payload)
+      .send()
+      .await?;
+
+    match response.status() {
+      StatusCode::OK => {
+        tracing::debug!("Successfully sent WNS notif to {}", &url);
+        Ok(())
+      }
+      error_status => {
+        let body = response
+          .text()
+          .await
+          .unwrap_or_else(|error| format!("Error occurred: {}", error));
+        tracing::error!(
+          "Failed sending WNS notification to: {}. Status: {}. Body: {}",
+          &url,
+          error_status,
+          body
+        );
+        let wns_error = WNSErrorResponse::from_status(error_status, body);
+        Err(error::Error::WNSNotification(wns_error))
+      }
+    }
+  }
+
+  pub async fn get_wns_token(&self) -> Result<String, error::Error> {
     const EXPIRY_WINDOW: Duration = Duration::from_secs(10);
 
     {
@@ -42,7 +88,7 @@
         .map_err(|_| error::Error::ReadLock)?;
       if let Some(ref token) = *read_guard {
         if token.expires >= SystemTime::now() - EXPIRY_WINDOW {
-          return Ok(Some(token.token.clone()));
+          return Ok(token.token.clone());
         }
       }
     }
@@ -68,17 +114,17 @@
         .await
         .unwrap_or_else(|_| String::from("<failed to read body>"));
       tracing::error!(status, "Failure when getting the WNS token: {}", body);
-      return Ok(None);
+      return Err(error::Error::WNSToken(WNSTokenError::Unknown(status)));
     }
 
     let response_json: serde_json::Value = response.json().await?;
     let token = response_json["access_token"]
       .as_str()
-      .ok_or(error::Error::TokenNotFound)?
+      .ok_or(error::Error::WNSToken(WNSTokenError::TokenNotFound))?
       .to_string();
     let expires_in = response_json["expires_in"]
       .as_u64()
-      .ok_or(error::Error::ExpiryNotFound)?;
+      .ok_or(error::Error::WNSToken(WNSTokenError::ExpiryNotFound))?;
 
     let expires = SystemTime::now() + Duration::from_secs(expires_in);
 
@@ -92,6 +138,6 @@
         expires,
       });
     }
-    Ok(Some(token))
+    Ok(token)
   }
 }
diff --git a/services/tunnelbroker/src/notifs/wns/response.rs b/services/tunnelbroker/src/notifs/wns/response.rs
new file mode 100644
--- /dev/null
+++ b/services/tunnelbroker/src/notifs/wns/response.rs
@@ -0,0 +1,77 @@
+use derive_more::{Display, Error};
+use reqwest::StatusCode;
+
+#[derive(PartialEq, Debug, Clone, Display, Error)]
+pub struct InvalidArgumentError {
+  pub details: String,
+}
+
+#[derive(PartialEq, Debug, Display, Error)]
+pub enum WNSErrorResponse {
+  /// No more information is available about this error.
+  UnspecifiedError,
+
+  /// HTTP error code = 400.
+  /// One or more headers were specified incorrectly or conflict with another
+  /// header.
+  BadRequest(InvalidArgumentError),
+
+  /// HTTP error code = 401.
+  /// The cloud service did not present a valid authentication ticket.
+  Unauthorized,
+
+  /// HTTP error code = 403.
+  /// The cloud service is not authorized to send a notification to this URI.
+  Forbidden,
+
+  /// HTTP error code = 404.
+  /// The channel URI is not valid or is not recognized by WNS.
+  NotFound,
+
+  /// HTTP error code = 405.
+  /// Invalid method (GET, CREATE); only POST (Windows or Windows Phone) or
+  /// DELETE (Windows Phone only) is allowed.
+  MethodNotAllowed,
+
+  /// HTTP error code = 406.
+  /// The cloud service exceeded its throttle limit.
+  NotAcceptable,
+
+  /// HTTP error code = 410.
+  /// The channel expired.
+  Gone,
+
+  /// HTTP error code = 413.
+  /// The notification payload exceeds the 5000 byte size limit.
+  RequestEntityTooLarge,
+
+  /// HTTP error code = 500.
+  /// An internal failure caused notification delivery to fail.
+  InternalServerError,
+
+  /// HTTP error code = 503.
+  /// The server is currently unavailable.
+  ServiceUnavailable,
+}
+
+impl WNSErrorResponse {
+  pub fn from_status(status: StatusCode, body: String) -> Self {
+    match status {
+      StatusCode::BAD_REQUEST => {
+        WNSErrorResponse::BadRequest(InvalidArgumentError { details: body })
+      }
+      StatusCode::UNAUTHORIZED => WNSErrorResponse::Unauthorized,
+      StatusCode::FORBIDDEN => WNSErrorResponse::Forbidden,
+      StatusCode::NOT_FOUND => WNSErrorResponse::NotFound,
+      StatusCode::METHOD_NOT_ALLOWED => WNSErrorResponse::MethodNotAllowed,
+      StatusCode::NOT_ACCEPTABLE => WNSErrorResponse::NotAcceptable,
+      StatusCode::GONE => WNSErrorResponse::Gone,
+      StatusCode::PAYLOAD_TOO_LARGE => WNSErrorResponse::RequestEntityTooLarge,
+      StatusCode::INTERNAL_SERVER_ERROR => {
+        WNSErrorResponse::InternalServerError
+      }
+      StatusCode::SERVICE_UNAVAILABLE => WNSErrorResponse::ServiceUnavailable,
+      _ => WNSErrorResponse::UnspecifiedError,
+    }
+  }
+}
diff --git a/services/tunnelbroker/src/websockets/session.rs b/services/tunnelbroker/src/websockets/session.rs
--- a/services/tunnelbroker/src/websockets/session.rs
+++ b/services/tunnelbroker/src/websockets/session.rs
@@ -40,6 +40,7 @@
   AndroidConfig, AndroidMessagePriority, FCMMessage,
 };
 use crate::notifs::web_push::WebPushNotif;
+use crate::notifs::wns::WNSNotif;
 use crate::notifs::{apns, NotifClient, NotifClientType};
 use crate::{identity, notifs};
 
@@ -77,6 +78,7 @@
   MissingAPNsClient,
   MissingFCMClient,
   MissingWebPushClient,
+  MissingWNSClient,
   MissingDeviceToken,
   InvalidDeviceToken,
   InvalidNotifProvider,
@@ -581,6 +583,46 @@
           self.get_message_to_device_status(&notif.client_message_id, result),
         )
       }
+      DeviceToTunnelbrokerMessage::WNSNotif(notif) => {
+        if !self.device_info.is_authenticated {
+          debug!(
+            "Unauthenticated device {} tried to send WNS notif. Aborting.",
+            self.device_info.device_id
+          );
+          return Some(MessageSentStatus::Unauthenticated);
+        }
+        debug!("Received WNS notif for {}", notif.device_id);
+
+        let Some(wns_client) = self.notif_client.wns.clone() else {
+          return Some(self.get_message_to_device_status(
+            &notif.client_message_id,
+            Err(SessionError::MissingWNSClient),
+          ));
+        };
+
+        let device_token = match self
+          .get_device_token(notif.device_id, NotifClientType::WNS)
+          .await
+        {
+          Ok(token) => token,
+          Err(e) => {
+            return Some(
+              self
+                .get_message_to_device_status(&notif.client_message_id, Err(e)),
+            )
+          }
+        };
+
+        let wns_notif = WNSNotif {
+          device_token,
+          payload: notif.payload,
+        };
+
+        let result = wns_client.send(wns_notif).await;
+        Some(
+          self.get_message_to_device_status(&notif.client_message_id, result),
+        )
+      }
       _ => {
         error!("Client sent invalid message type");
         Some(MessageSentStatus::InvalidRequest)
diff --git a/shared/tunnelbroker_messages/src/messages/mod.rs b/shared/tunnelbroker_messages/src/messages/mod.rs
--- a/shared/tunnelbroker_messages/src/messages/mod.rs
+++ b/shared/tunnelbroker_messages/src/messages/mod.rs
@@ -46,6 +46,7 @@
   APNsNotif(APNsNotif),
   FCMNotif(FCMNotif),
   WebPushNotif(WebPushNotif),
+  WNSNotif(WNSNotif),
   MessageToDeviceRequest(MessageToDeviceRequest),
   MessageReceiveConfirmation(MessageReceiveConfirmation),
   MessageToTunnelbrokerRequest(MessageToTunnelbrokerRequest),
diff --git a/shared/tunnelbroker_messages/src/messages/notif.rs b/shared/tunnelbroker_messages/src/messages/notif.rs
--- a/shared/tunnelbroker_messages/src/messages/notif.rs
+++ b/shared/tunnelbroker_messages/src/messages/notif.rs
@@ -35,3 +35,14 @@
   pub device_id: String,
   pub payload: String,
 }
+
+/// WNS notif built on client.
+#[derive(Serialize, Deserialize, TagAwareDeserialize, PartialEq, Debug)]
+#[serde(tag = "type", remote = "Self", rename_all = "camelCase")]
+pub struct WNSNotif {
+  #[serde(rename = "clientMessageID")]
+  pub client_message_id: String,
+  #[serde(rename = "deviceID")]
+  pub device_id: String,
+  pub payload: String,
+}