diff --git a/Examples/xx-messenger/Sources/UserSearchFeature/UserSearchFeature.swift b/Examples/xx-messenger/Sources/UserSearchFeature/UserSearchFeature.swift
index 2e892bb4882b43ddab7d91ab81d15c007e561a87..f39353a78db32d57a1429d8f8a0b6e5fb46260e0 100644
--- a/Examples/xx-messenger/Sources/UserSearchFeature/UserSearchFeature.swift
+++ b/Examples/xx-messenger/Sources/UserSearchFeature/UserSearchFeature.swift
@@ -41,7 +41,7 @@ public struct UserSearchState: Equatable {
 
   public init(
     focusedField: Field? = nil,
-    query: MessengerSearchUsers.Query = .init(),
+    query: MessengerSearchContacts.Query = .init(),
     isSearching: Bool = false,
     failure: String? = nil,
     results: IdentifiedArrayOf<Result> = [],
@@ -56,7 +56,7 @@ public struct UserSearchState: Equatable {
   }
 
   @BindableState public var focusedField: Field?
-  @BindableState public var query: MessengerSearchUsers.Query
+  @BindableState public var query: MessengerSearchContacts.Query
   public var isSearching: Bool
   public var failure: String?
   public var results: IdentifiedArrayOf<Result>
@@ -113,7 +113,7 @@ public let userSearchReducer = Reducer<UserSearchState, UserSearchAction, UserSe
     state.failure = nil
     return .result { [query = state.query] in
       do {
-        return .success(.didSucceed(try env.messenger.searchUsers(query: query)))
+        return .success(.didSucceed(try env.messenger.searchContacts(query: query)))
       } catch {
         return .success(.didFail(error.localizedDescription))
       }
diff --git a/Examples/xx-messenger/Sources/UserSearchFeature/UserSearchView.swift b/Examples/xx-messenger/Sources/UserSearchFeature/UserSearchView.swift
index b3a1f0a48be3ca73751906bd5c72e88363234535..328ff98c2e0f79907da76927ce254322022ea07a 100644
--- a/Examples/xx-messenger/Sources/UserSearchFeature/UserSearchView.swift
+++ b/Examples/xx-messenger/Sources/UserSearchFeature/UserSearchView.swift
@@ -14,7 +14,7 @@ public struct UserSearchView: View {
 
   struct ViewState: Equatable {
     var focusedField: UserSearchState.Field?
-    var query: MessengerSearchUsers.Query
+    var query: MessengerSearchContacts.Query
     var isSearching: Bool
     var failure: String?
     var results: IdentifiedArrayOf<UserSearchState.Result>
diff --git a/Examples/xx-messenger/Tests/UserSearchFeatureTests/UserSearchFeatureTests.swift b/Examples/xx-messenger/Tests/UserSearchFeatureTests/UserSearchFeatureTests.swift
index f8f482f6c49d236f511239a6722cc357ec48ab77..44731c1f16d41e0b95ab3ff1675f654d1ab2fcd5 100644
--- a/Examples/xx-messenger/Tests/UserSearchFeatureTests/UserSearchFeatureTests.swift
+++ b/Examples/xx-messenger/Tests/UserSearchFeatureTests/UserSearchFeatureTests.swift
@@ -13,7 +13,7 @@ final class UserSearchFeatureTests: XCTestCase {
       environment: .unimplemented
     )
 
-    var didSearchWithQuery: [MessengerSearchUsers.Query] = []
+    var didSearchWithQuery: [MessengerSearchContacts.Query] = []
 
     struct GetIdFromContactError: Error {}
     struct GetFactsFromContactError: Error {}
@@ -44,7 +44,7 @@ final class UserSearchFeatureTests: XCTestCase {
 
     store.environment.bgQueue = .immediate
     store.environment.mainQueue = .immediate
-    store.environment.messenger.searchUsers.run = { query in
+    store.environment.messenger.searchContacts.run = { query in
       didSearchWithQuery.append(query)
       return contacts
     }
@@ -100,7 +100,7 @@ final class UserSearchFeatureTests: XCTestCase {
 
     store.environment.bgQueue = .immediate
     store.environment.mainQueue = .immediate
-    store.environment.messenger.searchUsers.run = { _ in throw failure }
+    store.environment.messenger.searchContacts.run = { _ in throw failure }
 
     store.send(.searchTapped) {
       $0.focusedField = nil
diff --git a/Sources/XXClient/Callbacks/UdMultiLookupCallback.swift b/Sources/XXClient/Callbacks/UdMultiLookupCallback.swift
index 9a6d9df64e56f28edc072449bc0dac5be7de6aa4..1782c01ef83d2a74feef73f490e422f201c67c9a 100644
--- a/Sources/XXClient/Callbacks/UdMultiLookupCallback.swift
+++ b/Sources/XXClient/Callbacks/UdMultiLookupCallback.swift
@@ -2,9 +2,20 @@ import Bindings
 import XCTestDynamicOverlay
 
 public struct UdMultiLookupCallback {
-  public enum Result: Equatable {
-    case success([Contact])
-    case failure(error: NSError, failedIDs: [Data])
+  public struct Result: Equatable {
+    public init(
+      contacts: [Contact],
+      failedIds: [Data],
+      errors: [NSError]
+    ) {
+      self.contacts = contacts
+      self.failedIds = failedIds
+      self.errors = errors
+    }
+
+    public var contacts: [Contact]
+    public var failedIds: [Data]
+    public var errors: [NSError]
   }
 
   public init(handle: @escaping (Result) -> Void) {
@@ -30,23 +41,31 @@ extension UdMultiLookupCallback {
       let callback: UdMultiLookupCallback
 
       func callback(_ contactListJSON: Data?, failedIDs: Data?, err: Error?) {
+        var result = UdMultiLookupCallback.Result(
+          contacts: [],
+          failedIds: [],
+          errors: []
+        )
         if let err = err {
-          callback.handle(.failure(
-            error: err as NSError,
-            failedIDs: failedIDs
-              .map { (try? JSONDecoder().decode([Data].self, from: $0)) ?? [] } ?? []
-          ))
-        } else if let contactListJSON = contactListJSON {
+          result.errors.append(err as NSError)
+        }
+        if let contactListJSON = contactListJSON {
+          do {
+            result.contacts = try JSONDecoder()
+              .decode([Data].self, from: contactListJSON)
+              .map { Contact.live($0) }
+          } catch {
+            result.errors.append(error as NSError)
+          }
+        }
+        if let failedIDs = failedIDs {
           do {
-            let contactsData = try JSONDecoder().decode([Data].self, from: contactListJSON)
-            let contacts: [Contact] = contactsData.map { Contact.live($0) }
-            callback.handle(.success(contacts))
+            result.failedIds = try JSONDecoder().decode([Data].self, from: failedIDs)
           } catch {
-            callback.handle(.failure(error: error as NSError, failedIDs: []))
+            result.errors.append(error as NSError)
           }
-        } else {
-          fatalError("BindingsUdMultiLookupCallbackProtocol received `nil` contactListJSON and `nil` error")
         }
+        callback.handle(result)
       }
     }
 
diff --git a/Sources/XXClient/Functions/LookupUD.swift b/Sources/XXClient/Functions/LookupUD.swift
index 0626470c4662ce0ebc7111d227d7fa1c4b4ddaad..3a505bc7ea134822568d56371f2b0bf81f244416 100644
--- a/Sources/XXClient/Functions/LookupUD.swift
+++ b/Sources/XXClient/Functions/LookupUD.swift
@@ -2,30 +2,45 @@ import Bindings
 import XCTestDynamicOverlay
 
 public struct LookupUD {
-  public var run: (Int, Contact, Data, Data, UdLookupCallback) throws -> SingleUseSendReport
+  public struct Params: Equatable {
+    public init(
+      e2eId: Int,
+      udContact: Contact,
+      lookupId: Data,
+      singleRequestParamsJSON: Data = GetSingleUseParams.liveDefault()
+    ) {
+      self.e2eId = e2eId
+      self.udContact = udContact
+      self.lookupId = lookupId
+      self.singleRequestParamsJSON = singleRequestParamsJSON
+    }
+
+    public var e2eId: Int
+    public var udContact: Contact
+    public var lookupId: Data
+    public var singleRequestParamsJSON: Data
+  }
+
+  public var run: (Params, UdLookupCallback) throws -> SingleUseSendReport
 
   public func callAsFunction(
-    e2eId: Int,
-    udContact: Contact,
-    lookupId: Data,
-    singleRequestParamsJSON: Data = GetSingleUseParams.liveDefault(),
+    params: Params,
     callback: UdLookupCallback
   ) throws -> SingleUseSendReport {
-    try run(e2eId, udContact, lookupId, singleRequestParamsJSON, callback)
+    try run(params, callback)
   }
 }
 
 extension LookupUD {
-  public static let live = LookupUD {
-    e2eId, udContact, lookupId, singleRequestParamsJSON, callback in
+  public static let live = LookupUD { params, callback in
 
     var error: NSError?
     let reportData = BindingsLookupUD(
-      e2eId,
-      udContact.data,
+      params.e2eId,
+      params.udContact.data,
       callback.makeBindingsUdLookupCallback(),
-      lookupId,
-      singleRequestParamsJSON,
+      params.lookupId,
+      params.singleRequestParamsJSON,
       &error
     )
     if let error = error {
diff --git a/Sources/XXClient/Functions/SearchUD.swift b/Sources/XXClient/Functions/SearchUD.swift
index 9f82808d322bc5980f1dd8293b175ec04dd0e005..aa7c8b81f8b899882079ec35907713ee98ce3006 100644
--- a/Sources/XXClient/Functions/SearchUD.swift
+++ b/Sources/XXClient/Functions/SearchUD.swift
@@ -2,30 +2,44 @@ import Bindings
 import XCTestDynamicOverlay
 
 public struct SearchUD {
-  public var run: (Int, Contact, [Fact], Data, UdSearchCallback) throws -> SingleUseSendReport
+  public struct Params: Equatable {
+    public init(
+      e2eId: Int,
+      udContact: Contact,
+      facts: [Fact],
+      singleRequestParamsJSON: Data = GetSingleUseParams.liveDefault()
+    ) {
+      self.e2eId = e2eId
+      self.udContact = udContact
+      self.facts = facts
+      self.singleRequestParamsJSON = singleRequestParamsJSON
+    }
+
+    public var e2eId: Int
+    public var udContact: Contact
+    public var facts: [Fact]
+    public var singleRequestParamsJSON: Data
+  }
+
+  public var run: (Params, UdSearchCallback) throws -> SingleUseSendReport
 
   public func callAsFunction(
-    e2eId: Int,
-    udContact: Contact,
-    facts: [Fact],
-    singleRequestParamsJSON: Data = GetSingleUseParams.liveDefault(),
+    params: Params,
     callback: UdSearchCallback
   ) throws -> SingleUseSendReport {
-    try run(e2eId, udContact, facts, singleRequestParamsJSON, callback)
+    try run(params, callback)
   }
 }
 
 extension SearchUD {
-  public static let live = SearchUD {
-    e2eId, udContact, facts, singleRequestParamsJSON, callback in
-
+  public static let live = SearchUD { params, callback in
     var error: NSError?
     let reportData = BindingsSearchUD(
-      e2eId,
-      udContact.data,
+      params.e2eId,
+      params.udContact.data,
       callback.makeBindingsUdSearchCallback(),
-      try facts.encode(),
-      singleRequestParamsJSON,
+      try params.facts.encode(),
+      params.singleRequestParamsJSON,
       &error
     )
     if let error = error {
diff --git a/Sources/XXMessengerClient/Messenger/Functions/MessengerLookupContact.swift b/Sources/XXMessengerClient/Messenger/Functions/MessengerLookupContact.swift
new file mode 100644
index 0000000000000000000000000000000000000000..de77da419c70b98ae9df2665bae86ad4280d9eb6
--- /dev/null
+++ b/Sources/XXMessengerClient/Messenger/Functions/MessengerLookupContact.swift
@@ -0,0 +1,51 @@
+import Foundation
+import XCTestDynamicOverlay
+import XXClient
+
+public struct MessengerLookupContact {
+  public enum Error: Swift.Error, Equatable {
+    case notConnected
+    case notLoggedIn
+  }
+
+  public var run: (Data) throws -> Contact
+
+  public func callAsFunction(id: Data) throws -> Contact {
+    try run(id)
+  }
+}
+
+extension MessengerLookupContact {
+  public static func live(_ env: MessengerEnvironment) -> MessengerLookupContact {
+    MessengerLookupContact { id in
+      guard let e2e = env.e2e() else {
+        throw Error.notConnected
+      }
+      guard let ud = env.ud() else {
+        throw Error.notLoggedIn
+      }
+      var result: Result<Contact, NSError>!
+      let semaphore = DispatchSemaphore(value: 0)
+      _ = try env.lookupUD(
+        params: LookupUD.Params(
+          e2eId: e2e.getId(),
+          udContact: try ud.getContact(),
+          lookupId: id,
+          singleRequestParamsJSON: env.getSingleUseParams()
+        ),
+        callback: UdLookupCallback { lookupResult in
+          result = lookupResult
+          semaphore.signal()
+        }
+      )
+      semaphore.wait()
+      return try result.get()
+    }
+  }
+}
+
+extension MessengerLookupContact {
+  public static let unimplemented = MessengerLookupContact(
+    run: XCTUnimplemented("\(Self.self)")
+  )
+}
diff --git a/Sources/XXMessengerClient/Messenger/Functions/MessengerLookupContacts.swift b/Sources/XXMessengerClient/Messenger/Functions/MessengerLookupContacts.swift
new file mode 100644
index 0000000000000000000000000000000000000000..77a667db6d9b0c15a6d4bf97067879fe8c48b791
--- /dev/null
+++ b/Sources/XXMessengerClient/Messenger/Functions/MessengerLookupContacts.swift
@@ -0,0 +1,51 @@
+import Foundation
+import XCTestDynamicOverlay
+import XXClient
+
+public struct MessengerLookupContacts {
+  public enum Error: Swift.Error, Equatable {
+    case notConnected
+    case notLoggedIn
+  }
+
+  public var run: ([Data]) throws -> UdMultiLookupCallback.Result
+
+  public func callAsFunction(ids: [Data]) throws -> UdMultiLookupCallback.Result {
+    try run(ids)
+  }
+}
+
+extension MessengerLookupContacts {
+  public static func live(_ env: MessengerEnvironment) -> MessengerLookupContacts {
+    MessengerLookupContacts { ids in
+      guard let e2e = env.e2e() else {
+        throw Error.notConnected
+      }
+      guard let ud = env.ud() else {
+        throw Error.notLoggedIn
+      }
+      var callbackResult: UdMultiLookupCallback.Result!
+      let semaphore = DispatchSemaphore(value: 0)
+      _ = try env.multiLookupUD(
+        params: MultiLookupUD.Params(
+          e2eId: e2e.getId(),
+          udContact: try ud.getContact(),
+          lookupIds: ids,
+          singleRequestParams: env.getSingleUseParams()
+        ),
+        callback: UdMultiLookupCallback { result in
+          callbackResult = result
+          semaphore.signal()
+        }
+      )
+      semaphore.wait()
+      return callbackResult
+    }
+  }
+}
+
+extension MessengerLookupContacts {
+  public static let unimplemented = MessengerLookupContacts(
+    run: XCTUnimplemented("\(Self.self)")
+  )
+}
diff --git a/Sources/XXMessengerClient/Messenger/Functions/MessengerSearchUsers.swift b/Sources/XXMessengerClient/Messenger/Functions/MessengerSearchContacts.swift
similarity index 77%
rename from Sources/XXMessengerClient/Messenger/Functions/MessengerSearchUsers.swift
rename to Sources/XXMessengerClient/Messenger/Functions/MessengerSearchContacts.swift
index 5acd76d7c9b05d2c4eee1c01effdbefbcd4be3a7..46b2007c5a61501de402636be51834ac4c006540 100644
--- a/Sources/XXMessengerClient/Messenger/Functions/MessengerSearchUsers.swift
+++ b/Sources/XXMessengerClient/Messenger/Functions/MessengerSearchContacts.swift
@@ -2,7 +2,7 @@ import Foundation
 import XCTestDynamicOverlay
 import XXClient
 
-public struct MessengerSearchUsers {
+public struct MessengerSearchContacts {
   public struct Query: Equatable {
     public init(
       username: String? = nil,
@@ -31,9 +31,9 @@ public struct MessengerSearchUsers {
   }
 }
 
-extension MessengerSearchUsers {
-  public static func live(_ env: MessengerEnvironment) -> MessengerSearchUsers {
-    MessengerSearchUsers { query in
+extension MessengerSearchContacts {
+  public static func live(_ env: MessengerEnvironment) -> MessengerSearchContacts {
+    MessengerSearchContacts { query in
       guard let e2e = env.e2e() else {
         throw Error.notConnected
       }
@@ -43,11 +43,13 @@ extension MessengerSearchUsers {
       var result: Result<[Contact], Swift.Error>!
       let semaphore = DispatchSemaphore(value: 0)
       _ = try env.searchUD(
-        e2eId: e2e.getId(),
-        udContact: try ud.getContact(),
-        facts: query.facts,
-        singleRequestParamsJSON: env.getSingleUseParams(),
-        callback: .init { searchResult in
+        params: SearchUD.Params(
+          e2eId: e2e.getId(),
+          udContact: try ud.getContact(),
+          facts: query.facts,
+          singleRequestParamsJSON: env.getSingleUseParams()
+        ),
+        callback: UdSearchCallback { searchResult in
           switch searchResult {
           case .success(let contacts):
             result = .success(contacts)
@@ -63,7 +65,7 @@ extension MessengerSearchUsers {
   }
 }
 
-extension MessengerSearchUsers.Query {
+extension MessengerSearchContacts.Query {
   public var isEmpty: Bool {
     [username, email, phone]
       .compactMap { $0 }
@@ -86,8 +88,8 @@ extension MessengerSearchUsers.Query {
   }
 }
 
-extension MessengerSearchUsers {
-  public static let unimplemented = MessengerSearchUsers(
+extension MessengerSearchContacts {
+  public static let unimplemented = MessengerSearchContacts(
     run: XCTUnimplemented("\(Self.self)")
   )
 }
diff --git a/Sources/XXMessengerClient/Messenger/Functions/MessengerVerifyContact.swift b/Sources/XXMessengerClient/Messenger/Functions/MessengerVerifyContact.swift
index 7851dd1f122204b83c9fc45c282c9ff1e4a71c37..afb13e7c28cd21bbe446702dc6f21fa7bd777d0e 100644
--- a/Sources/XXMessengerClient/Messenger/Functions/MessengerVerifyContact.swift
+++ b/Sources/XXMessengerClient/Messenger/Functions/MessengerVerifyContact.swift
@@ -30,11 +30,13 @@ extension MessengerVerifyContact {
         var lookupResult: Result<Contact, NSError>!
         let semaphore = DispatchSemaphore(value: 0)
         _ = try env.lookupUD(
-          e2eId: e2e.getId(),
-          udContact: try ud.getContact(),
-          lookupId: try contact.getId(),
-          singleRequestParamsJSON: env.getSingleUseParams(),
-          callback: .init { result in
+          params: LookupUD.Params(
+            e2eId: e2e.getId(),
+            udContact: try ud.getContact(),
+            lookupId: try contact.getId(),
+            singleRequestParamsJSON: env.getSingleUseParams()
+          ),
+          callback: UdLookupCallback { result in
             lookupResult = result
             semaphore.signal()
           }
@@ -45,11 +47,13 @@ extension MessengerVerifyContact {
         var searchResult: Result<[Contact], NSError>!
         let semaphore = DispatchSemaphore(value: 0)
         _ = try env.searchUD(
-          e2eId: e2e.getId(),
-          udContact: try ud.getContact(),
-          facts: facts,
-          singleRequestParamsJSON: env.getSingleUseParams(),
-          callback: .init { result in
+          params: SearchUD.Params(
+            e2eId: e2e.getId(),
+            udContact: try ud.getContact(),
+            facts: facts,
+            singleRequestParamsJSON: env.getSingleUseParams()
+          ),
+          callback: UdSearchCallback { result in
             searchResult = result
             semaphore.signal()
           }
diff --git a/Sources/XXMessengerClient/Messenger/Messenger.swift b/Sources/XXMessengerClient/Messenger/Messenger.swift
index 4995f9a14c02e1b1b0c5fe8181d02cb0f689704c..7186b6119270ff96da5af712b03d50d0912c91fb 100644
--- a/Sources/XXMessengerClient/Messenger/Messenger.swift
+++ b/Sources/XXMessengerClient/Messenger/Messenger.swift
@@ -21,7 +21,8 @@ public struct Messenger {
   public var waitForNetwork: MessengerWaitForNetwork
   public var waitForNodes: MessengerWaitForNodes
   public var destroy: MessengerDestroy
-  public var searchUsers: MessengerSearchUsers
+  public var searchContacts: MessengerSearchContacts
+  public var lookupContact: MessengerLookupContact
   public var registerForNotifications: MessengerRegisterForNotifications
   public var verifyContact: MessengerVerifyContact
   public var sendMessage: MessengerSendMessage
@@ -50,7 +51,8 @@ extension Messenger {
       waitForNetwork: .live(env),
       waitForNodes: .live(env),
       destroy: .live(env),
-      searchUsers: .live(env),
+      searchContacts: .live(env),
+      lookupContact: .live(env),
       registerForNotifications: .live(env),
       verifyContact: .live(env),
       sendMessage: .live(env)
@@ -80,7 +82,8 @@ extension Messenger {
     waitForNetwork: .unimplemented,
     waitForNodes: .unimplemented,
     destroy: .unimplemented,
-    searchUsers: .unimplemented,
+    searchContacts: .unimplemented,
+    lookupContact: .unimplemented,
     registerForNotifications: .unimplemented,
     verifyContact: .unimplemented,
     sendMessage: .unimplemented
diff --git a/Sources/XXMessengerClient/Messenger/MessengerEnvironment.swift b/Sources/XXMessengerClient/Messenger/MessengerEnvironment.swift
index b4ff607ed6238450cdbf05aec918376c833060cb..f125c1ef3ae705a1dde5d8e9ff1dc4467e25044c 100644
--- a/Sources/XXMessengerClient/Messenger/MessengerEnvironment.swift
+++ b/Sources/XXMessengerClient/Messenger/MessengerEnvironment.swift
@@ -17,6 +17,7 @@ public struct MessengerEnvironment {
   public var login: Login
   public var lookupUD: LookupUD
   public var messageListeners: ListenersRegistry
+  public var multiLookupUD: MultiLookupUD
   public var ndfEnvironment: NDFEnvironment
   public var newCMix: NewCMix
   public var newOrLoadUd: NewOrLoadUd
@@ -54,6 +55,7 @@ extension MessengerEnvironment {
       login: .live,
       lookupUD: .live,
       messageListeners: .live(),
+      multiLookupUD: .live(),
       ndfEnvironment: .mainnet,
       newCMix: .live,
       newOrLoadUd: .live,
@@ -86,6 +88,7 @@ extension MessengerEnvironment {
     login: .unimplemented,
     lookupUD: .unimplemented,
     messageListeners: .unimplemented,
+    multiLookupUD: .unimplemented,
     ndfEnvironment: .unimplemented,
     newCMix: .unimplemented,
     newOrLoadUd: .unimplemented,
diff --git a/Tests/XXMessengerClientTests/Messenger/Functions/MessengerLookupContactTests.swift b/Tests/XXMessengerClientTests/Messenger/Functions/MessengerLookupContactTests.swift
new file mode 100644
index 0000000000000000000000000000000000000000..3069e8c034e393da5c2522583ab68ea52e5f7079
--- /dev/null
+++ b/Tests/XXMessengerClientTests/Messenger/Functions/MessengerLookupContactTests.swift
@@ -0,0 +1,117 @@
+import CustomDump
+import XCTest
+import XXClient
+@testable import XXMessengerClient
+
+final class MessengerLookupContactTests: XCTestCase {
+  func testLookup() throws {
+    let contactId = "contact-id".data(using: .utf8)!
+    let e2eId = 123
+    let udContact = Contact.unimplemented("ud-contact".data(using: .utf8)!)
+    let singleRequestParams = "single-request-params".data(using: .utf8)!
+    let contact = Contact.unimplemented("contact".data(using: .utf8)!)
+
+    var didLookupWithParams: [LookupUD.Params] = []
+
+    var env: MessengerEnvironment = .unimplemented
+    env.e2e.get = {
+      var e2e: E2E = .unimplemented
+      e2e.getId.run = { e2eId }
+      return e2e
+    }
+    env.ud.get = {
+      var ud: UserDiscovery = .unimplemented
+      ud.getContact.run = { udContact }
+      return ud
+    }
+    env.getSingleUseParams.run = { singleRequestParams }
+    env.lookupUD.run = { params, callback in
+      didLookupWithParams.append(params)
+      callback.handle(.success(contact))
+      return SingleUseSendReport(rounds: [], roundURL: "", ephId: 0, receptionId: Data())
+    }
+    let lookup: MessengerLookupContact = .live(env)
+
+    let result = try lookup(id: contactId)
+
+    XCTAssertNoDifference(didLookupWithParams, [.init(
+      e2eId: e2eId,
+      udContact: udContact,
+      lookupId: contactId,
+      singleRequestParamsJSON: singleRequestParams
+    )])
+    XCTAssertNoDifference(result, contact)
+  }
+
+  func testLookupWhenNotConnected() {
+    var env: MessengerEnvironment = .unimplemented
+    env.e2e.get = { nil }
+    let lookup: MessengerLookupContact = .live(env)
+
+    XCTAssertThrowsError(try lookup(id: Data())) { error in
+      XCTAssertEqual(error as? MessengerLookupContact.Error, .notConnected)
+    }
+  }
+
+  func testLookupWhenNotLoggedIn() {
+    var env: MessengerEnvironment = .unimplemented
+    env.e2e.get = { .unimplemented }
+    env.ud.get = { nil }
+    let lookup: MessengerLookupContact = .live(env)
+
+    XCTAssertThrowsError(try lookup(id: Data())) { error in
+      XCTAssertEqual(error as? MessengerLookupContact.Error, .notLoggedIn)
+    }
+  }
+
+  func testLookupFailure() {
+    struct Failure: Error, Equatable {}
+    let failure = Failure()
+
+    var env: MessengerEnvironment = .unimplemented
+    env.e2e.get = {
+      var e2e: E2E = .unimplemented
+      e2e.getId.run = { 0 }
+      return e2e
+    }
+    env.ud.get = {
+      var ud: UserDiscovery = .unimplemented
+      ud.getContact.run = { .unimplemented(Data()) }
+      return ud
+    }
+    env.getSingleUseParams.run = { Data() }
+    env.lookupUD.run = { _, _ in throw failure }
+    let lookup: MessengerLookupContact = .live(env)
+
+    XCTAssertThrowsError(try lookup(id: Data())) { error in
+      XCTAssertEqual(error as? Failure, failure)
+    }
+  }
+
+  func testLookupCallbackFailure() {
+    struct Failure: Error, Equatable {}
+    let failure = Failure()
+
+    var env: MessengerEnvironment = .unimplemented
+    env.e2e.get = {
+      var e2e: E2E = .unimplemented
+      e2e.getId.run = { 0 }
+      return e2e
+    }
+    env.ud.get = {
+      var ud: UserDiscovery = .unimplemented
+      ud.getContact.run = { .unimplemented(Data()) }
+      return ud
+    }
+    env.getSingleUseParams.run = { Data() }
+    env.lookupUD.run = { _, callback in
+      callback.handle(.failure(failure as NSError))
+      return SingleUseSendReport(rounds: [], roundURL: "", ephId: 0, receptionId: Data())
+    }
+    let lookup: MessengerLookupContact = .live(env)
+
+    XCTAssertThrowsError(try lookup(id: Data())) { error in
+      XCTAssertEqual(error as? Failure, failure)
+    }
+  }
+}
diff --git a/Tests/XXMessengerClientTests/Messenger/Functions/MessengerLookupContactsTests.swift b/Tests/XXMessengerClientTests/Messenger/Functions/MessengerLookupContactsTests.swift
new file mode 100644
index 0000000000000000000000000000000000000000..9f9031103eefa21bb4e85434a6efa436b681b9c4
--- /dev/null
+++ b/Tests/XXMessengerClientTests/Messenger/Functions/MessengerLookupContactsTests.swift
@@ -0,0 +1,93 @@
+import CustomDump
+import XCTest
+import XXClient
+@testable import XXMessengerClient
+
+final class MessengerLookupContactsTests: XCTestCase {
+  func testLookup() throws {
+    let contactIds = ["contact-id".data(using: .utf8)!]
+    let e2eId = 123
+    let udContact = Contact.unimplemented("ud-contact".data(using: .utf8)!)
+    let singleRequestParams = "single-request-params".data(using: .utf8)!
+    let contacts = [Contact.unimplemented("contact".data(using: .utf8)!)]
+
+    var didMultiLookupWithParams: [MultiLookupUD.Params] = []
+
+    var env: MessengerEnvironment = .unimplemented
+    env.e2e.get = {
+      var e2e: E2E = .unimplemented
+      e2e.getId.run = { e2eId }
+      return e2e
+    }
+    env.ud.get = {
+      var ud: UserDiscovery = .unimplemented
+      ud.getContact.run = { udContact }
+      return ud
+    }
+    env.getSingleUseParams.run = { singleRequestParams }
+    env.multiLookupUD.run = { params, callback in
+      didMultiLookupWithParams.append(params)
+      callback.handle(.init(
+        contacts: contacts,
+        failedIds: [],
+        errors: []
+      ))
+    }
+    let lookup: MessengerLookupContacts = .live(env)
+
+    let result = try lookup(ids: contactIds)
+
+    XCTAssertNoDifference(didMultiLookupWithParams, [.init(
+      e2eId: e2eId,
+      udContact: udContact,
+      lookupIds: contactIds,
+      singleRequestParams: singleRequestParams
+    )])
+    XCTAssertNoDifference(result, .init(contacts: contacts, failedIds: [], errors: []))
+  }
+
+  func testLookupWhenNotConnected() {
+    var env: MessengerEnvironment = .unimplemented
+    env.e2e.get = { nil }
+    let lookup: MessengerLookupContacts = .live(env)
+
+    XCTAssertThrowsError(try lookup(ids: [])) { error in
+      XCTAssertEqual(error as? MessengerLookupContacts.Error, .notConnected)
+    }
+  }
+
+  func testLookupWhenNotLoggedIn() {
+    var env: MessengerEnvironment = .unimplemented
+    env.e2e.get = { .unimplemented }
+    env.ud.get = { nil }
+    let lookup: MessengerLookupContacts = .live(env)
+
+    XCTAssertThrowsError(try lookup(ids: [])) { error in
+      XCTAssertEqual(error as? MessengerLookupContacts.Error, .notLoggedIn)
+    }
+  }
+
+  func testLookupFailure() {
+    struct Failure: Error, Equatable {}
+    let failure = Failure()
+
+    var env: MessengerEnvironment = .unimplemented
+    env.e2e.get = {
+      var e2e: E2E = .unimplemented
+      e2e.getId.run = { 0 }
+      return e2e
+    }
+    env.ud.get = {
+      var ud: UserDiscovery = .unimplemented
+      ud.getContact.run = { .unimplemented(Data()) }
+      return ud
+    }
+    env.getSingleUseParams.run = { Data() }
+    env.multiLookupUD.run = { _, _ in throw failure }
+    let lookup: MessengerLookupContacts = .live(env)
+
+    XCTAssertThrowsError(try lookup(ids: [])) { error in
+      XCTAssertEqual(error as? Failure, failure)
+    }
+  }
+}
diff --git a/Tests/XXMessengerClientTests/Messenger/Functions/MessengerSearchUsersTests.swift b/Tests/XXMessengerClientTests/Messenger/Functions/MessengerSearchContactsTests.swift
similarity index 75%
rename from Tests/XXMessengerClientTests/Messenger/Functions/MessengerSearchUsersTests.swift
rename to Tests/XXMessengerClientTests/Messenger/Functions/MessengerSearchContactsTests.swift
index 21152086ca38c771ae678109a4545be1232c5b9f..2b44f19eadbdcffaa527d07bbbd1ea5234497754 100644
--- a/Tests/XXMessengerClientTests/Messenger/Functions/MessengerSearchUsersTests.swift
+++ b/Tests/XXMessengerClientTests/Messenger/Functions/MessengerSearchContactsTests.swift
@@ -3,16 +3,9 @@ import XCTest
 import XXClient
 @testable import XXMessengerClient
 
-final class MessengerSearchUsersTests: XCTestCase {
+final class MessengerSearchContactsTests: XCTestCase {
   func testSearch() throws {
-    struct SearchUdParams: Equatable {
-      var e2eId: Int
-      var udContact: Contact
-      var facts: [Fact]
-      var singleRequestParamsJSON: Data
-    }
-
-    var didSearchUdWithParams: [SearchUdParams] = []
+    var didSearchUdWithParams: [SearchUD.Params] = []
 
     var env: MessengerEnvironment = .unimplemented
     env.e2e.get = {
@@ -26,13 +19,8 @@ final class MessengerSearchUsersTests: XCTestCase {
       return ud
     }
     env.getSingleUseParams.run = { "single-use-params".data(using: .utf8)! }
-    env.searchUD.run = { e2eId, udContact, facts, singleRequestParamsJSON, callback in
-      didSearchUdWithParams.append(.init(
-        e2eId: e2eId,
-        udContact: udContact,
-        facts: facts,
-        singleRequestParamsJSON: singleRequestParamsJSON
-      ))
+    env.searchUD.run = { params, callback in
+      didSearchUdWithParams.append(params)
       callback.handle(.success([
         .unimplemented("contact-1".data(using: .utf8)!),
         .unimplemented("contact-2".data(using: .utf8)!),
@@ -41,8 +29,8 @@ final class MessengerSearchUsersTests: XCTestCase {
       return SingleUseSendReport(rounds: [], roundURL: "", ephId: 0, receptionId: Data())
     }
 
-    let search: MessengerSearchUsers = .live(env)
-    let query = MessengerSearchUsers.Query(
+    let search: MessengerSearchContacts = .live(env)
+    let query = MessengerSearchContacts.Query(
       username: "Username",
       email: "Email",
       phone: "Phone"
@@ -67,10 +55,10 @@ final class MessengerSearchUsersTests: XCTestCase {
   func testSearchNotConnected() {
     var env: MessengerEnvironment = .unimplemented
     env.e2e.get = { nil }
-    let search: MessengerSearchUsers = .live(env)
+    let search: MessengerSearchContacts = .live(env)
 
     XCTAssertThrowsError(try search(query: .init())) { error in
-      XCTAssertNoDifference(error as? MessengerSearchUsers.Error, .notConnected)
+      XCTAssertNoDifference(error as? MessengerSearchContacts.Error, .notConnected)
     }
   }
 
@@ -78,10 +66,10 @@ final class MessengerSearchUsersTests: XCTestCase {
     var env: MessengerEnvironment = .unimplemented
     env.e2e.get = { .unimplemented }
     env.ud.get = { nil }
-    let search: MessengerSearchUsers = .live(env)
+    let search: MessengerSearchContacts = .live(env)
 
     XCTAssertThrowsError(try search(query: .init())) { error in
-      XCTAssertNoDifference(error as? MessengerSearchUsers.Error, .notLoggedIn)
+      XCTAssertNoDifference(error as? MessengerSearchContacts.Error, .notLoggedIn)
     }
   }
 
@@ -101,9 +89,9 @@ final class MessengerSearchUsersTests: XCTestCase {
       return ud
     }
     env.getSingleUseParams.run = { Data() }
-    env.searchUD.run = { _, _, _, _, _ in throw error }
+    env.searchUD.run = { _, _ in throw error }
 
-    let search: MessengerSearchUsers = .live(env)
+    let search: MessengerSearchContacts = .live(env)
 
     XCTAssertThrowsError(try search(query: .init())) { err in
       XCTAssertNoDifference(err as? Failure, error)
@@ -126,12 +114,12 @@ final class MessengerSearchUsersTests: XCTestCase {
       return ud
     }
     env.getSingleUseParams.run = { Data() }
-    env.searchUD.run = { _, _, _, _, callback in
+    env.searchUD.run = { _, callback in
       callback.handle(.failure(error as NSError))
       return SingleUseSendReport(rounds: [], roundURL: "", ephId: 0, receptionId: Data())
     }
 
-    let search: MessengerSearchUsers = .live(env)
+    let search: MessengerSearchContacts = .live(env)
 
     XCTAssertThrowsError(try search(query: .init())) { err in
       XCTAssertNoDifference(err as? Failure, error)
@@ -139,7 +127,7 @@ final class MessengerSearchUsersTests: XCTestCase {
   }
 
   func testQueryIsEmpty() {
-    let emptyQueries: [MessengerSearchUsers.Query] = [
+    let emptyQueries: [MessengerSearchContacts.Query] = [
       .init(username: nil, email: nil, phone: nil),
       .init(username: "", email: nil, phone: nil),
       .init(username: nil, email: "", phone: nil),
@@ -151,7 +139,7 @@ final class MessengerSearchUsersTests: XCTestCase {
       XCTAssertTrue(query.isEmpty, "\(query) should be empty")
     }
 
-    let nonEmptyQueries: [MessengerSearchUsers.Query] = [
+    let nonEmptyQueries: [MessengerSearchContacts.Query] = [
       .init(username: "test", email: nil, phone: nil),
       .init(username: nil, email: "test", phone: nil),
       .init(username: nil, email: nil, phone: "test"),
@@ -165,17 +153,17 @@ final class MessengerSearchUsersTests: XCTestCase {
 
   func testQueryFacts() {
     XCTAssertNoDifference(
-      MessengerSearchUsers.Query(username: nil, email: nil, phone: nil).facts,
+      MessengerSearchContacts.Query(username: nil, email: nil, phone: nil).facts,
       []
     )
 
     XCTAssertNoDifference(
-      MessengerSearchUsers.Query(username: "", email: "", phone: "").facts,
+      MessengerSearchContacts.Query(username: "", email: "", phone: "").facts,
       []
     )
 
     XCTAssertNoDifference(
-      MessengerSearchUsers.Query(
+      MessengerSearchContacts.Query(
         username: "username",
         email: "email",
         phone: "phone"
@@ -188,7 +176,7 @@ final class MessengerSearchUsersTests: XCTestCase {
     )
 
     XCTAssertNoDifference(
-      MessengerSearchUsers.Query(
+      MessengerSearchContacts.Query(
         username: "username",
         email: "",
         phone: nil
diff --git a/Tests/XXMessengerClientTests/Messenger/Functions/MessengerVerifyContactTests.swift b/Tests/XXMessengerClientTests/Messenger/Functions/MessengerVerifyContactTests.swift
index 52fed6cb730831a1cdaf6ad41d924533355e643c..92e4142a667d96d2862287afbd28dc76132577ca 100644
--- a/Tests/XXMessengerClientTests/Messenger/Functions/MessengerVerifyContactTests.swift
+++ b/Tests/XXMessengerClientTests/Messenger/Functions/MessengerVerifyContactTests.swift
@@ -28,18 +28,12 @@ final class MessengerVerifyContactTests: XCTestCase {
   }
 
   func testVerifyContactWithoutFacts() throws {
-    struct LookupUDParams: Equatable {
-      var e2eId: Int
-      var udContact: Contact
-      var lookupId: Data
-      var singleRequestParamsJSON: Data
-    }
     struct VerifyOwnershipParams: Equatable {
       var received: Contact
       var verified: Contact
       var e2eId: Int
     }
-    var didLookupUDWithParams: [LookupUDParams] = []
+    var didLookupUDWithParams: [LookupUD.Params] = []
     var didVerifyOwnershipWithParams: [VerifyOwnershipParams] = []
 
     var contact = Contact.unimplemented("contact-data".data(using: .utf8)!)
@@ -68,13 +62,8 @@ final class MessengerVerifyContactTests: XCTestCase {
       return ud
     }
     env.getSingleUseParams.run = { "single-use-params".data(using: .utf8)! }
-    env.lookupUD.run = { e2eId, udContact, lookupId, singleRequestParamsJSON, callback in
-      didLookupUDWithParams.append(.init(
-        e2eId: e2eId,
-        udContact: udContact,
-        lookupId: lookupId,
-        singleRequestParamsJSON: singleRequestParamsJSON
-      ))
+    env.lookupUD.run = { params, callback in
+      didLookupUDWithParams.append(params)
       callback.handle(.success(lookedUpContact))
       return SingleUseSendReport(rounds: [], roundURL: "", ephId: 0, receptionId: Data())
     }
@@ -117,7 +106,7 @@ final class MessengerVerifyContactTests: XCTestCase {
       return ud
     }
     env.getSingleUseParams.run = { "single-use-params".data(using: .utf8)! }
-    env.lookupUD.run = { _, _, _, _, callback in
+    env.lookupUD.run = { _, callback in
       callback.handle(.failure(lookupFailure))
       return SingleUseSendReport(rounds: [], roundURL: "", ephId: 0, receptionId: Data())
     }
@@ -129,18 +118,12 @@ final class MessengerVerifyContactTests: XCTestCase {
   }
 
   func testVerifyContactWithFacts() throws {
-    struct SearchUDParams: Equatable {
-      var e2eId: Int
-      var udContact: Contact
-      var facts: [Fact]
-      var singleRequestParamsJSON: Data
-    }
     struct VerifyOwnershipParams: Equatable {
       var received: Contact
       var verified: Contact
       var e2eId: Int
     }
-    var didSearchUDWithParams: [SearchUDParams] = []
+    var didSearchUDWithParams: [SearchUD.Params] = []
     var didVerifyOwnershipWithParams: [VerifyOwnershipParams] = []
 
     var contact = Contact.unimplemented("contact-data".data(using: .utf8)!)
@@ -174,13 +157,8 @@ final class MessengerVerifyContactTests: XCTestCase {
       return ud
     }
     env.getSingleUseParams.run = { "single-use-params".data(using: .utf8)! }
-    env.searchUD.run = { e2eId, udContact, facts, singleRequestParamsJSON, callback in
-      didSearchUDWithParams.append(.init(
-        e2eId: e2eId,
-        udContact: udContact,
-        facts: facts,
-        singleRequestParamsJSON: singleRequestParamsJSON
-      ))
+    env.searchUD.run = { params, callback in
+      didSearchUDWithParams.append(params)
       callback.handle(.success([foundContact]))
       return SingleUseSendReport(rounds: [], roundURL: "", ephId: 0, receptionId: Data())
     }
@@ -226,7 +204,7 @@ final class MessengerVerifyContactTests: XCTestCase {
       return ud
     }
     env.getSingleUseParams.run = { "single-use-params".data(using: .utf8)! }
-    env.searchUD.run = { e2eId, udContact, facts, singleRequestParamsJSON, callback in
+    env.searchUD.run = { _, callback in
       callback.handle(.success([]))
       return SingleUseSendReport(rounds: [], roundURL: "", ephId: 0, receptionId: Data())
     }
@@ -262,7 +240,7 @@ final class MessengerVerifyContactTests: XCTestCase {
       return ud
     }
     env.getSingleUseParams.run = { "single-use-params".data(using: .utf8)! }
-    env.searchUD.run = { _, _, _, _, callback in
+    env.searchUD.run = { _, callback in
       callback.handle(.failure(searchFailure))
       return SingleUseSendReport(rounds: [], roundURL: "", ephId: 0, receptionId: Data())
     }
@@ -296,7 +274,7 @@ final class MessengerVerifyContactTests: XCTestCase {
       return ud
     }
     env.getSingleUseParams.run = { "single-use-params".data(using: .utf8)! }
-    env.searchUD.run = { _, _, _, _, callback in
+    env.searchUD.run = { _, callback in
       callback.handle(.success([.unimplemented("found-contact-data".data(using: .utf8)!)]))
       return SingleUseSendReport(rounds: [], roundURL: "", ephId: 0, receptionId: Data())
     }
@@ -335,7 +313,7 @@ final class MessengerVerifyContactTests: XCTestCase {
       return ud
     }
     env.getSingleUseParams.run = { "single-use-params".data(using: .utf8)! }
-    env.searchUD.run = { _, _, _, _, callback in
+    env.searchUD.run = { _, callback in
       callback.handle(.success([.unimplemented("found-contact-data".data(using: .utf8)!)]))
       return SingleUseSendReport(rounds: [], roundURL: "", ephId: 0, receptionId: Data())
     }