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/Messenger.swift b/Sources/XXMessengerClient/Messenger/Messenger.swift
index 6a3c417b4f93a325f988a8b64fd012f31b15a339..7186b6119270ff96da5af712b03d50d0912c91fb 100644
--- a/Sources/XXMessengerClient/Messenger/Messenger.swift
+++ b/Sources/XXMessengerClient/Messenger/Messenger.swift
@@ -22,6 +22,7 @@ public struct Messenger {
   public var waitForNodes: MessengerWaitForNodes
   public var destroy: MessengerDestroy
   public var searchContacts: MessengerSearchContacts
+  public var lookupContact: MessengerLookupContact
   public var registerForNotifications: MessengerRegisterForNotifications
   public var verifyContact: MessengerVerifyContact
   public var sendMessage: MessengerSendMessage
@@ -51,6 +52,7 @@ extension Messenger {
       waitForNodes: .live(env),
       destroy: .live(env),
       searchContacts: .live(env),
+      lookupContact: .live(env),
       registerForNotifications: .live(env),
       verifyContact: .live(env),
       sendMessage: .live(env)
@@ -81,6 +83,7 @@ extension Messenger {
     waitForNodes: .unimplemented,
     destroy: .unimplemented,
     searchContacts: .unimplemented,
+    lookupContact: .unimplemented,
     registerForNotifications: .unimplemented,
     verifyContact: .unimplemented,
     sendMessage: .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)
+    }
+  }
+}