diff --git a/services/identity/Cargo.lock b/services/identity/Cargo.lock
--- a/services/identity/Cargo.lock
+++ b/services/identity/Cargo.lock
@@ -74,7 +74,7 @@
 dependencies = [
  "proc-macro2",
  "quote",
- "syn 2.0.13",
+ "syn 2.0.28",
 ]
 
 [[package]]
@@ -852,7 +852,7 @@
  "proc-macro2",
  "quote",
  "scratch",
- "syn 2.0.13",
+ "syn 2.0.28",
 ]
 
 [[package]]
@@ -869,7 +869,7 @@
 dependencies = [
  "proc-macro2",
  "quote",
- "syn 2.0.13",
+ "syn 2.0.28",
 ]
 
 [[package]]
@@ -1147,7 +1147,7 @@
 dependencies = [
  "proc-macro2",
  "quote",
- "syn 2.0.13",
+ "syn 2.0.28",
 ]
 
 [[package]]
@@ -1468,6 +1468,7 @@
  "tonic-web",
  "tracing",
  "tracing-subscriber",
+ "tunnelbroker_messages",
  "uuid",
 ]
 
@@ -1949,9 +1950,9 @@
 
 [[package]]
 name = "proc-macro2"
-version = "1.0.56"
+version = "1.0.66"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2b63bdb0cd06f1f4dedf69b254734f9b45af66e4a031e42a7480257d9898b435"
+checksum = "18fb31db3f9bddb2ea821cde30a9f70117e3f119938b5ee630b7403aa6e2ead9"
 dependencies = [
  "unicode-ident",
 ]
@@ -2039,9 +2040,9 @@
 
 [[package]]
 name = "quote"
-version = "1.0.26"
+version = "1.0.32"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4424af4bf778aae2051a77b60283332f386554255d722233d09fbfc7e30da2fc"
+checksum = "50f3b39ccfb720540debaa0164757101c08ecb8d326b15358ce76a62c7e85965"
 dependencies = [
  "proc-macro2",
 ]
@@ -2355,29 +2356,29 @@
 
 [[package]]
 name = "serde"
-version = "1.0.159"
+version = "1.0.182"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3c04e8343c3daeec41f58990b9d77068df31209f2af111e059e9fe9646693065"
+checksum = "bdb30a74471f5b7a1fa299f40b4bf1be93af61116df95465b2b5fc419331e430"
 dependencies = [
  "serde_derive",
 ]
 
 [[package]]
 name = "serde_derive"
-version = "1.0.159"
+version = "1.0.182"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4c614d17805b093df4b147b51339e7e44bf05ef59fba1e45d83500bcfb4d8585"
+checksum = "6f4c2c6ea4bc09b5c419012eafcdb0fcef1d9119d626c8f3a0708a5b92d38a70"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn 2.0.13",
+ "syn 2.0.28",
 ]
 
 [[package]]
 name = "serde_json"
-version = "1.0.95"
+version = "1.0.104"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d721eca97ac802aa7777b701877c8004d950fc142651367300d21c1cc0194744"
+checksum = "076066c5f1078eac5b722a31827a8832fe108bed65dfa75e233c89f8206e976c"
 dependencies = [
  "itoa",
  "ryu",
@@ -2526,9 +2527,9 @@
 
 [[package]]
 name = "syn"
-version = "2.0.13"
+version = "2.0.28"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4c9da457c5285ac1f936ebd076af6dac17a61cfe7826f2076b4d015cf47bc8ec"
+checksum = "04361975b3f5e348b2189d8dc55bc942f278b2d482a6a0365de5bdd62d351567"
 dependencies = [
  "proc-macro2",
  "quote",
@@ -2592,7 +2593,7 @@
 dependencies = [
  "proc-macro2",
  "quote",
- "syn 2.0.13",
+ "syn 2.0.28",
 ]
 
 [[package]]
@@ -2677,7 +2678,7 @@
 dependencies = [
  "proc-macro2",
  "quote",
- "syn 2.0.13",
+ "syn 2.0.28",
 ]
 
 [[package]]
@@ -2902,6 +2903,14 @@
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "3528ecfd12c466c6f163363caf2d02a71161dd5e1cc6ae7b34207ea2d42d81ed"
 
+[[package]]
+name = "tunnelbroker_messages"
+version = "0.1.0"
+dependencies = [
+ "serde",
+ "serde_json",
+]
+
 [[package]]
 name = "typenum"
 version = "1.16.0"
@@ -3326,5 +3335,5 @@
 dependencies = [
  "proc-macro2",
  "quote",
- "syn 2.0.13",
+ "syn 2.0.28",
 ]
diff --git a/services/identity/Cargo.toml b/services/identity/Cargo.toml
--- a/services/identity/Cargo.toml
+++ b/services/identity/Cargo.toml
@@ -25,6 +25,7 @@
 tonic-web = "0.9.1"
 serde = { version = "1.0.159", features = [ "derive" ] }
 serde_json = "1.0.95"
+tunnelbroker_messages = { path = "../../shared/tunnelbroker_messages" }
 moka = { version = "0.10", features = ["future"] }
 uuid = { version = "1.3", features = [ "v4" ] }
 base64 = "0.21.2"
diff --git a/services/identity/build.rs b/services/identity/build.rs
--- a/services/identity/build.rs
+++ b/services/identity/build.rs
@@ -9,5 +9,12 @@
       ],
       &["../../shared/protos/"],
     )?;
+  tonic_build::configure()
+    .build_server(false)
+    .build_client(true)
+    .compile(
+      &["../../shared/protos/tunnelbroker.proto"],
+      &["../../shared/protos/"],
+    )?;
   Ok(())
 }
diff --git a/services/identity/src/constants.rs b/services/identity/src/constants.rs
--- a/services/identity/src/constants.rs
+++ b/services/identity/src/constants.rs
@@ -130,3 +130,10 @@
 // Tunnelbroker
 pub const TUNNELBROKER_GRPC_ENDPOINT: &str = "TUNNELBROKER_GRPC_ENDPOINT";
 pub const DEFAULT_TUNNELBROKER_ENDPOINT: &str = "http://localhost:50051";
+
+// X3DH key management
+
+// Threshold for requesting more onetime keys
+pub const ONETIME_KEY_MINIMUM_THRESHOLD: usize = 5;
+// Number of keys to be refreshed when below the threshold
+pub const ONETIME_KEY_REFRESH_NUMBER: u32 = 5;
diff --git a/services/identity/src/database.rs b/services/identity/src/database.rs
--- a/services/identity/src/database.rs
+++ b/services/identity/src/database.rs
@@ -7,7 +7,7 @@
 use crate::ddb_utils::{
   create_one_time_key_partition_key, into_one_time_put_requests, OlmAccountType,
 };
-use crate::error::{DBItemAttributeError, DBItemError, Error};
+use crate::error::{consume_error, DBItemAttributeError, DBItemError, Error};
 use aws_config::SdkConfig;
 use aws_sdk_dynamodb::model::{AttributeValue, PutRequest, WriteRequest};
 use aws_sdk_dynamodb::output::{
@@ -379,22 +379,32 @@
     account_type: OlmAccountType,
   ) -> Result<Option<String>, Error> {
     use crate::constants::one_time_keys_table as otk_table;
+    use crate::constants::ONETIME_KEY_MINIMUM_THRESHOLD;
 
     let query_result = self.get_onetime_keys(device_id, account_type).await?;
     let items = query_result.items();
 
     // If no onetime keys exists, return none early
-    if items.is_none() {
+    let Some(item_vec) = items else {
       debug!("Unable to find {:?} onetime-key", account_type);
       return Ok(None);
+    };
+
+    if item_vec.len() < ONETIME_KEY_MINIMUM_THRESHOLD {
+      // Avoid device_id being moved out-of-scope by "move"
+      let device_id = device_id.to_string();
+      tokio::spawn(async move {
+        debug!("Attempting to request more keys for device: {}", &device_id);
+        let result =
+          crate::tunnelbroker::send_refresh_keys_request(&device_id).await;
+        consume_error(result);
+      });
     }
 
     let mut result = None;
-
-    // "items" was checked to be None above, will be safe to unwrap here.
     // Attempt to delete the onetime keys individually, a successful delete
     // mints the onetime key to the requester
-    for item in items.unwrap() {
+    for item in item_vec {
       let pk = item.get_string(otk_table::PARTITION_KEY)?;
       let otk = item.get_string(otk_table::SORT_KEY)?;
 
diff --git a/services/identity/src/error.rs b/services/identity/src/error.rs
--- a/services/identity/src/error.rs
+++ b/services/identity/src/error.rs
@@ -1,6 +1,7 @@
 use aws_sdk_dynamodb::{model::AttributeValue, Error as DynamoDBError};
 use std::collections::hash_map::HashMap;
 use std::fmt::{Display, Formatter, Result as FmtResult};
+use tracing::error;
 
 #[derive(
   Debug, derive_more::Display, derive_more::From, derive_more::Error,
@@ -11,6 +12,10 @@
   #[display(...)]
   Attribute(DBItemError),
   #[display(...)]
+  Transport(tonic::transport::Error),
+  #[display(...)]
+  Status(tonic::Status),
+  #[display(...)]
   MissingItem,
 }
 
@@ -141,3 +146,12 @@
       .to_vec(key)
   }
 }
+
+pub fn consume_error<T>(result: Result<T, Error>) {
+  match result {
+    Ok(_) => return,
+    Err(e) => {
+      error!("{}", e);
+    }
+  }
+}
diff --git a/services/identity/src/main.rs b/services/identity/src/main.rs
--- a/services/identity/src/main.rs
+++ b/services/identity/src/main.rs
@@ -18,6 +18,7 @@
 mod reserved_users;
 mod siwe;
 mod token;
+mod tunnelbroker;
 
 use config::load_config;
 use constants::{IDENTITY_SERVICE_SOCKET_ADDR, SECRETS_DIRECTORY};
diff --git a/services/identity/src/tunnelbroker.rs b/services/identity/src/tunnelbroker.rs
new file mode 100644
--- /dev/null
+++ b/services/identity/src/tunnelbroker.rs
@@ -0,0 +1,48 @@
+mod proto {
+  tonic::include_proto!("tunnelbroker");
+}
+use crate::config::CONFIG;
+use proto::tunnelbroker_service_client::TunnelbrokerServiceClient;
+use proto::{Empty, MessageToDevice};
+use tonic::transport::Channel;
+use tonic::Response;
+use tracing::error;
+use tunnelbroker_messages as messages;
+
+use crate::error::Error;
+
+pub async fn create_tunnelbroker_client(
+) -> Result<TunnelbrokerServiceClient<Channel>, Error> {
+  TunnelbrokerServiceClient::connect(CONFIG.tunnelbroker_endpoint.to_string())
+    .await
+    .map_err(|e| {
+      error!("Unable able to connect to tunnelbroker: {:?}", e);
+      e.into()
+    })
+}
+
+pub async fn send_refresh_keys_request(
+  device_id: &str,
+) -> Result<Response<Empty>, Error> {
+  use crate::constants::ONETIME_KEY_REFRESH_NUMBER;
+
+  let mut tunnelbroker_client = create_tunnelbroker_client().await?;
+
+  let refresh_request = messages::RefreshKeyRequest {
+    device_id: device_id.to_string(),
+    number_of_keys: ONETIME_KEY_REFRESH_NUMBER,
+  };
+
+  let payload = serde_json::to_string(&refresh_request).unwrap();
+  let request = MessageToDevice {
+    device_id: device_id.to_string(),
+    payload,
+  };
+
+  let grpc_message = tonic::Request::new(request);
+  Ok(
+    tunnelbroker_client
+      .send_message_to_device(grpc_message)
+      .await?,
+  )
+}