diff --git a/lib/actions/user-actions.js b/lib/actions/user-actions.js
--- a/lib/actions/user-actions.js
+++ b/lib/actions/user-actions.js
@@ -15,6 +15,9 @@
   ClaimUsernameResponse,
   LogInResponse,
   LogInRequest,
+  KeyserverAuthResult,
+  KeyserverAuthInfo,
+  KeyserverAuthRequest,
 } from '../types/account-types.js';
 import type {
   UpdateUserAvatarRequest,
@@ -223,6 +226,121 @@
     };
   };
 
+const keyserverAuthActionTypes = Object.freeze({
+  started: 'KEYSERVER_AUTH_STARTED',
+  success: 'KEYSERVER_AUTH_SUCCESS',
+  failed: 'KEYSERVER_AUTH_FAILED',
+});
+const keyserverAuthCallServerEndpointOptions = { timeout: 60000 };
+const keyserverAuth =
+  (
+    callKeyserverEndpoint: CallKeyserverEndpoint,
+  ): ((input: KeyserverAuthInfo) => Promise<KeyserverAuthResult>) =>
+  async keyserverAuthInfo => {
+    const watchedIDs = threadWatcher.getWatchedIDs();
+
+    const {
+      logInActionSource,
+      calendarQuery,
+      keyserverData,
+      deviceTokenUpdateInput,
+      ...restLogInInfo
+    } = keyserverAuthInfo;
+
+    const keyserverIDs = Object.keys(keyserverData);
+
+    const watchedIDsPerKeyserver = sortThreadIDsPerKeyserver(watchedIDs);
+    const calendarQueryPerKeyserver = sortCalendarQueryPerKeyserver(
+      calendarQuery,
+      keyserverIDs,
+    );
+
+    const requests: { [string]: KeyserverAuthRequest } = {};
+    for (const keyserverID of keyserverIDs) {
+      requests[keyserverID] = {
+        ...restLogInInfo,
+        deviceTokenUpdateRequest: deviceTokenUpdateInput[keyserverID],
+        watchedIDs: watchedIDsPerKeyserver[keyserverID] ?? [],
+        calendarQuery: calendarQueryPerKeyserver[keyserverID],
+        platformDetails: getConfig().platformDetails,
+        initialContentEncryptedMessage:
+          keyserverData[keyserverID].initialContentEncryptedMessage,
+        initialNotificationsEncryptedMessage:
+          keyserverData[keyserverID].initialNotificationsEncryptedMessage,
+        source: logInActionSource,
+      };
+    }
+
+    const responses: { +[string]: LogInResponse } = await callKeyserverEndpoint(
+      'keyserver_auth',
+      requests,
+      keyserverAuthCallServerEndpointOptions,
+    );
+
+    const userInfosArrays = [];
+
+    let threadInfos: RawThreadInfos = {};
+    const calendarResult: WritableCalendarResult = {
+      calendarQuery: keyserverAuthInfo.calendarQuery,
+      rawEntryInfos: [],
+    };
+    const messagesResult: WritableGenericMessagesResult = {
+      messageInfos: [],
+      truncationStatus: {},
+      watchedIDsAtRequestTime: watchedIDs,
+      currentAsOf: {},
+    };
+    let updatesCurrentAsOf: { +[string]: number } = {};
+    for (const keyserverID in responses) {
+      threadInfos = {
+        ...responses[keyserverID].cookieChange.threadInfos,
+        ...threadInfos,
+      };
+      if (responses[keyserverID].rawEntryInfos) {
+        calendarResult.rawEntryInfos = calendarResult.rawEntryInfos.concat(
+          responses[keyserverID].rawEntryInfos,
+        );
+      }
+      messagesResult.messageInfos = messagesResult.messageInfos.concat(
+        responses[keyserverID].rawMessageInfos,
+      );
+      messagesResult.truncationStatus = {
+        ...messagesResult.truncationStatus,
+        ...responses[keyserverID].truncationStatuses,
+      };
+      messagesResult.currentAsOf = {
+        ...messagesResult.currentAsOf,
+        [keyserverID]: responses[keyserverID].serverTime,
+      };
+      updatesCurrentAsOf = {
+        ...updatesCurrentAsOf,
+        [keyserverID]: responses[keyserverID].serverTime,
+      };
+      userInfosArrays.push(responses[keyserverID].userInfos);
+      userInfosArrays.push(responses[keyserverID].cookieChange.userInfos);
+    }
+
+    const userInfos = mergeUserInfos(...userInfosArrays);
+
+    return {
+      threadInfos,
+      currentUserInfo: responses[ashoatKeyserverID].currentUserInfo,
+      calendarResult,
+      messagesResult,
+      userInfos,
+      updatesCurrentAsOf,
+      logInActionSource: keyserverAuthInfo.logInActionSource,
+      notAcknowledgedPolicies:
+        responses[ashoatKeyserverID].notAcknowledgedPolicies,
+    };
+  };
+
+function useKeyserverAuth(): (
+  input: KeyserverAuthInfo,
+) => Promise<KeyserverAuthResult> {
+  return useKeyserverCall(keyserverAuth);
+}
+
 function mergeUserInfos(
   ...userInfoArrays: Array<$ReadOnlyArray<UserInfo>>
 ): UserInfo[] {
@@ -564,4 +682,6 @@
   setAccessTokenActionType,
   deleteIdentityAccountActionTypes,
   useDeleteIdentityAccount,
+  keyserverAuthActionTypes,
+  useKeyserverAuth,
 };
diff --git a/lib/types/account-types.js b/lib/types/account-types.js
--- a/lib/types/account-types.js
+++ b/lib/types/account-types.js
@@ -96,6 +96,8 @@
   logInFromNativeSIWE: 'LOG_IN_FROM_NATIVE_SIWE',
   corruptedDatabaseDeletion: 'CORRUPTED_DATABASE_DELETION',
   refetchUserDataAfterAcknowledgment: 'REFETCH_USER_DATA_AFTER_ACKNOWLEDGMENT',
+  keyserverAuthFromNative: 'KEYSERVER_AUTH_FROM_NATIVE',
+  keyserverAuthFromWeb: 'KEYSERVER_AUTH_FROM_WEB',
 });
 
 export type LogInActionSource = $Values<typeof logInActionSources>;
@@ -159,6 +161,44 @@
   +notAcknowledgedPolicies?: $ReadOnlyArray<PolicyType>,
 };
 
+export type KeyserverAuthResult = {
+  +threadInfos: RawThreadInfos,
+  +currentUserInfo?: ?LoggedInUserInfo,
+  +messagesResult: GenericMessagesResult,
+  +userInfos: $ReadOnlyArray<UserInfo>,
+  +calendarResult: CalendarResult,
+  +updatesCurrentAsOf: { +[keyserverID: string]: number },
+  +logInActionSource: LogInActionSource,
+  +notAcknowledgedPolicies?: ?$ReadOnlyArray<PolicyType>,
+};
+
+type KeyserverRequestData = {
+  +initialContentEncryptedMessage: string,
+  +initialNotificationsEncryptedMessage: string,
+};
+
+export type KeyserverAuthInfo = {
+  +userID: string,
+  +deviceID: string,
+  +doNotRegister: boolean,
+  +calendarQuery: CalendarQuery,
+  +deviceTokenUpdateInput: DeviceTokenUpdateInput,
+  +logInActionSource: LogInActionSource,
+  +keyserverData: { +[keyserverID: string]: KeyserverRequestData },
+};
+
+export type KeyserverAuthRequest = $ReadOnly<{
+  ...KeyserverRequestData,
+  +userID: string,
+  +deviceID: string,
+  +doNotRegister: boolean,
+  +calendarQuery: CalendarQuery,
+  +deviceTokenUpdateRequest?: ?DeviceTokenUpdateRequest,
+  +watchedIDs: $ReadOnlyArray<string>,
+  +platformDetails: PlatformDetails,
+  +source?: LogInActionSource,
+}>;
+
 export type UpdatePasswordRequest = {
   code: string,
   password: string,
diff --git a/lib/types/endpoints.js b/lib/types/endpoints.js
--- a/lib/types/endpoints.js
+++ b/lib/types/endpoints.js
@@ -25,6 +25,7 @@
   LOG_IN: 'log_in',
   UPDATE_PASSWORD: 'update_password',
   POLICY_ACKNOWLEDGMENT: 'policy_acknowledgment',
+  KEYSERVER_AUTH: 'keyserver_auth',
 });
 type SessionChangingEndpoint = $Values<typeof sessionChangingEndpoints>;