diff --git a/keyserver/src/socket/tunnelbroker.js b/keyserver/src/socket/tunnelbroker.js
--- a/keyserver/src/socket/tunnelbroker.js
+++ b/keyserver/src/socket/tunnelbroker.js
@@ -2,11 +2,17 @@
 
 import WebSocket from 'ws';
 
-import { type TBKeyserverConnectionInitializationMessage } from 'lib/types/tunnelbroker-messages.js';
+import {
+  type TBKeyserverConnectionInitializationMessage,
+  type TBMessage,
+  tunnelbrokerMessageTypes,
+} from 'lib/types/tunnelbroker-messages.js';
+import { ServerError } from 'lib/utils/errors.js';
 import sleep from 'lib/utils/sleep.js';
 
 import { fetchOlmAccount } from '../updaters/olm-account-updater.js';
-import type { IdentityInfo } from '../user/identity.js';
+import { type IdentityInfo } from '../user/identity.js';
+import { uploadNewOneTimeKeys } from '../utils/olm-utils.js';
 
 async function createAndMaintainTunnelbrokerWebsocket(
   identityInfo: IdentityInfo,
@@ -21,6 +27,15 @@
   );
 }
 
+function handleTBMessageEvent(event: ArrayBuffer): Promise<void> {
+  const message: TBMessage = JSON.parse(event.toString());
+
+  if (message.type === tunnelbrokerMessageTypes.REFRESH_KEYS_REQUEST) {
+    return uploadNewOneTimeKeys(message.numberOfKeys);
+  }
+  throw new ServerError('unsupported_tunnelbroker_message');
+}
+
 function openTunnelbrokerConnection(
   deviceID: string,
   userID: string,
@@ -52,6 +67,8 @@
     tunnelbrokerSocket.on('error', (error: Error) => {
       console.error('Tunnelbroker socket error', error.message);
     });
+
+    tunnelbrokerSocket.on('message', handleTBMessageEvent);
   } catch {
     console.log('Failed to open connection with Tunnelbroker');
   }
diff --git a/keyserver/src/utils/olm-utils.js b/keyserver/src/utils/olm-utils.js
--- a/keyserver/src/utils/olm-utils.js
+++ b/keyserver/src/utils/olm-utils.js
@@ -13,8 +13,10 @@
   olmEncryptedMessageTypes,
   type OLMOneTimeKeys,
 } from 'lib/types/crypto-types.js';
+import { ServerError } from 'lib/utils/errors.js';
 import { values } from 'lib/utils/objects.js';
 
+import { fetchCallUpdateOlmAccount } from '../updaters/olm-account-updater.js';
 import { fetchIdentityInfo } from '../user/identity.js';
 
 type PickledOlmAccount = {
@@ -177,6 +179,45 @@
   notifAccount.mark_prekey_as_published();
 }
 
+async function uploadNewOneTimeKeys(numberOfKeys: number) {
+  const [rustAPI, identityInfo] = await Promise.all([
+    getRustAPI(),
+    fetchIdentityInfo(),
+  ]);
+
+  if (!identityInfo) {
+    throw new ServerError('missing_identity_info');
+  }
+
+  await fetchCallUpdateOlmAccount('content', (contentAccount: OlmAccount) => {
+    contentAccount.generate_one_time_keys(numberOfKeys);
+    const contentOneTimeKeys = getOneTimeKeyValues(
+      contentAccount.one_time_keys(),
+    );
+    const deviceID = JSON.parse(contentAccount.identity_keys()).curve25519;
+
+    return fetchCallUpdateOlmAccount(
+      'notifications',
+      async (notifAccount: OlmAccount) => {
+        notifAccount.generate_one_time_keys(numberOfKeys);
+        const notifOneTimeKeys = getOneTimeKeyValues(
+          notifAccount.one_time_keys(),
+        );
+        await rustAPI.uploadOneTimeKeys(
+          identityInfo.userId,
+          deviceID,
+          identityInfo.accessToken,
+          contentOneTimeKeys,
+          notifOneTimeKeys,
+        );
+
+        notifAccount.mark_keys_as_published();
+        contentAccount.mark_keys_as_published();
+      },
+    );
+  });
+}
+
 // DEPRECATED: revalidateAccountPrekeys should be used instead
 function validateAccountPrekey(account: OlmAccount) {
   if (shouldRotatePrekey(account)) {
@@ -200,4 +241,5 @@
   validateAccountPrekey,
   revalidateAccountPrekeys,
   publishNewPrekeys,
+  uploadNewOneTimeKeys,
 };
diff --git a/lib/types/tunnelbroker-messages.js b/lib/types/tunnelbroker-messages.js
--- a/lib/types/tunnelbroker-messages.js
+++ b/lib/types/tunnelbroker-messages.js
@@ -28,3 +28,17 @@
   | TBKeyserverConnectionInitializationMessage
   | TBClientConnectionInitializationMessage
   | TBNotifyClientConnectionInitializationMessage;
+
+export const tunnelbrokerMessageTypes = Object.freeze({
+  REFRESH_KEYS_REQUEST: 'RefreshKeyRequest',
+});
+
+export type TBRefreshKeysRequest = {
+  +type: 'RefreshKeyRequest',
+  +deviceId: string,
+  +numberOfKeys: number,
+};
+
+// Disjoint enumeration of all messages
+// Currently, only a single message
+export type TBMessage = TBRefreshKeysRequest;