diff --git a/Docs/XXMessengerClient.md b/Docs/XXMessengerClient.md
index efbb9de768ea89616473b3df51837785ee880dd4..c2f407c3702f5a04620dd5df035323cda3743ac1 100644
--- a/Docs/XXMessengerClient.md
+++ b/Docs/XXMessengerClient.md
@@ -155,4 +155,65 @@ let restoredEmail = facts.get(.email)?.value
 let restoredPhone = facts.get(.phone)?.value
 ```
 
-If no error was thrown during restoration, the `Messenger` is already loaded, started, connected, and logged in.
\ No newline at end of file
+If no error was thrown during restoration, the `Messenger` is already loaded, started, connected, and logged in.
+
+## 🚢 File transfers
+
+### Setup for receiving files
+
+```swift
+// register receive file callback before starting file transfer manager:
+let cancellable = messenger.registerReceiveFileCallback(.init { result in
+  switch result {
+  case .success(let receivedFile):
+    // handle file metadata...
+
+    // start receiving file data:
+    try! messenger.receiveFile(.init(transferId: receivedFile.transferId)) { info in
+      switch info {
+      case .progress(let transmitted, let total):
+        // handle progress...
+
+      case .finished(let data):
+        // handle received file data...
+
+      case .failed(let error):
+        // handle error...
+      }
+    }
+
+  case .failure(let error):
+    // handle error...
+  }
+})
+
+// start file transfer manager:
+try messenger.startFileTransfer()
+```
+
+### Send files
+
+Make sure to call `messenger.startFileTransfer` before sending files.
+
+```swift
+let file = FileSend(
+  name: ...,
+  type: ...,
+  preview: ...,
+  contents: ...
+)
+
+// send file:
+let transferId = try messenger.sendFile(.init(file: file, recipientId: ...)) { info in
+  switch info {
+  case .progress(let transferId, let transmitted, let total):
+    // handle progress...
+
+  case .finished(let transferId):
+    // handle completion...
+
+  case .failed(let transferId, let error):
+    // handle error...
+  }
+}
+```
diff --git a/Examples/xx-messenger/XXMessenger.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Examples/xx-messenger/XXMessenger.xcworkspace/xcshareddata/swiftpm/Package.resolved
index 51664bd603e976bab27b158f1ef45ac937dd18b7..482d554fe8adf685c8dc7ce64c3f7d09ccbf25e0 100644
--- a/Examples/xx-messenger/XXMessenger.xcworkspace/xcshareddata/swiftpm/Package.resolved
+++ b/Examples/xx-messenger/XXMessenger.xcworkspace/xcshareddata/swiftpm/Package.resolved
@@ -50,8 +50,8 @@
       "kind" : "remoteSourceControl",
       "location" : "https://github.com/pointfreeco/swift-case-paths",
       "state" : {
-        "revision" : "7346701ea29da0a85d4403cf3d7a589a58ae3dee",
-        "version" : "0.9.2"
+        "revision" : "15bba50ebf3a2065388c8d12210debe4f6ada202",
+        "version" : "0.10.0"
       }
     },
     {
@@ -68,8 +68,8 @@
       "kind" : "remoteSourceControl",
       "location" : "https://github.com/pointfreeco/swift-composable-architecture.git",
       "state" : {
-        "revision" : "9ea8c763061287052a68d5e6723fed45e898b7d9",
-        "version" : "0.40.2"
+        "revision" : "5c476994eaa79af8e466041f6de1ab116f37c528",
+        "version" : "0.42.0"
       }
     },
     {
diff --git a/Sources/XXClient/Callbacks/ReceiveFileCallback.swift b/Sources/XXClient/Callbacks/ReceiveFileCallback.swift
index 3f414934745ad2f651fb024adbc496033cee8f23..2c1d94be22e7a3a63a6590e985481c2f437ee777 100644
--- a/Sources/XXClient/Callbacks/ReceiveFileCallback.swift
+++ b/Sources/XXClient/Callbacks/ReceiveFileCallback.swift
@@ -2,11 +2,13 @@ import Bindings
 import XCTestDynamicOverlay
 
 public struct ReceiveFileCallback {
-  public init(handle: @escaping (Result<ReceivedFile, NSError>) -> Void) {
+  public typealias Result = Swift.Result<ReceivedFile, NSError>
+
+  public init(handle: @escaping (Result) -> Void) {
     self.handle = handle
   }
 
-  public var handle: (Result<ReceivedFile, NSError>) -> Void
+  public var handle: (Result) -> Void
 }
 
 extension ReceiveFileCallback {
diff --git a/Sources/XXMessengerClient/Messenger/Functions/MessengerReceiveFile.swift b/Sources/XXMessengerClient/Messenger/Functions/MessengerReceiveFile.swift
new file mode 100644
index 0000000000000000000000000000000000000000..ef4290f829d7811c4e0c94c31afa812c095edfbc
--- /dev/null
+++ b/Sources/XXMessengerClient/Messenger/Functions/MessengerReceiveFile.swift
@@ -0,0 +1,87 @@
+import Foundation
+import XCTestDynamicOverlay
+import XXClient
+
+public struct MessengerReceiveFile {
+  public struct Params: Equatable {
+    public init(
+      transferId: Data,
+      callbackIntervalMS: Int = 250
+    ) {
+      self.transferId = transferId
+      self.callbackIntervalMS = callbackIntervalMS
+    }
+
+    public var transferId: Data
+    public var callbackIntervalMS: Int
+  }
+
+  public enum CallbackInfo: Equatable {
+    public enum Failure: Equatable {
+      case callbackError(NSError)
+      case progressError(String)
+      case receiveError(NSError)
+    }
+
+    case progress(transmitted: Int, total: Int)
+    case finished(Data)
+    case failed(Failure)
+  }
+
+  public typealias Callback = (CallbackInfo) -> Void
+
+  public enum Error: Swift.Error, Equatable {
+    case fileTransferNotStarted
+  }
+
+  public var run: (Params, @escaping Callback) throws -> Void
+
+  public func callAsFunction(
+    _ params: Params,
+    callback: @escaping Callback
+  ) throws -> Void {
+    try run(params, callback)
+  }
+}
+
+extension MessengerReceiveFile {
+  public static func live(_ env: MessengerEnvironment) -> MessengerReceiveFile {
+    MessengerReceiveFile { params, callback in
+      guard let fileTransfer = env.fileTransfer() else {
+        throw Error.fileTransferNotStarted
+      }
+      try fileTransfer.registerReceivedProgressCallback(
+        transferId: params.transferId,
+        period: params.callbackIntervalMS,
+        callback: FileTransferProgressCallback { result in
+          switch result {
+          case .success(let info):
+            if let error = info.progress.error {
+              callback(.failed(.progressError(error)))
+            } else if info.progress.completed {
+              do {
+                callback(.finished(try fileTransfer.receive(transferId: params.transferId)))
+              } catch {
+                callback(.failed(.receiveError(error as NSError)))
+              }
+            } else {
+              callback(.progress(
+                transmitted: info.progress.transmitted,
+                total: info.progress.total
+              ))
+            }
+
+          case .failure(let error):
+            callback(.failed(.callbackError(error)))
+          }
+        }
+      )
+    }
+  }
+}
+
+extension MessengerReceiveFile {
+  public static let unimplemented = MessengerReceiveFile(
+    run: XCTUnimplemented("\(Self.self)")
+  )
+}
diff --git a/Sources/XXMessengerClient/Messenger/Functions/MessengerRegisterReceiveFileCallback.swift b/Sources/XXMessengerClient/Messenger/Functions/MessengerRegisterReceiveFileCallback.swift
new file mode 100644
index 0000000000000000000000000000000000000000..e5dddd8a038f9c16fe9e9b6f562b95048cfee78a
--- /dev/null
+++ b/Sources/XXMessengerClient/Messenger/Functions/MessengerRegisterReceiveFileCallback.swift
@@ -0,0 +1,24 @@
+import XCTestDynamicOverlay
+import XXClient
+
+public struct MessengerRegisterReceiveFileCallback {
+  public var run: (ReceiveFileCallback) -> Cancellable
+
+  public func callAsFunction(_ callback: ReceiveFileCallback) -> Cancellable {
+    run(callback)
+  }
+}
+
+extension MessengerRegisterReceiveFileCallback {
+  public static func live(_ env: MessengerEnvironment) -> MessengerRegisterReceiveFileCallback {
+    MessengerRegisterReceiveFileCallback { callback in
+      env.receiveFileCallbacks.register(callback)
+    }
+  }
+}
+
+extension MessengerRegisterReceiveFileCallback {
+  public static let unimplemented = MessengerRegisterReceiveFileCallback(
+    run: XCTUnimplemented("\(Self.self)", placeholder: Cancellable {})
+  )
+}
diff --git a/Sources/XXMessengerClient/Messenger/Functions/MessengerSendFile.swift b/Sources/XXMessengerClient/Messenger/Functions/MessengerSendFile.swift
index 8aca65c4a59f7d55d350f5ee8c9b189d1217fbe5..fde8607c42e6e7f6d8d4c97da9beca79e588c589 100644
--- a/Sources/XXMessengerClient/Messenger/Functions/MessengerSendFile.swift
+++ b/Sources/XXMessengerClient/Messenger/Functions/MessengerSendFile.swift
@@ -37,7 +37,7 @@ public struct MessengerSendFile {
   public typealias Callback = (CallbackInfo) -> Void
 
   public enum Error: Swift.Error, Equatable {
-    case notConnected
+    case fileTransferNotStarted
   }
 
   public var run: (Params, @escaping Callback) throws -> Data
@@ -53,19 +53,9 @@ public struct MessengerSendFile {
 extension MessengerSendFile {
   public static func live(_ env: MessengerEnvironment) -> MessengerSendFile {
     MessengerSendFile { params, callback in
-      guard let e2e = env.e2e() else {
-        throw Error.notConnected
+      guard let fileTransfer = env.fileTransfer() else {
+        throw Error.fileTransferNotStarted
       }
-      let fileTransfer = try env.initFileTransfer(
-        params: InitFileTransfer.Params(
-          e2eId: e2e.getId(),
-          e2eFileTransferParamsJSON: env.getE2EFileTransferParams(),
-          fileTransferParamsJSON: env.getFileTransferParams()
-        ),
-        callback: ReceiveFileCallback { _ in
-          fatalError("Bindings issue: ReceiveFileCallback called when sending file.")
-        }
-      )
       func close(id: Data) {
         do {
           try fileTransfer.closeSend(transferId: id)
diff --git a/Sources/XXMessengerClient/Messenger/Functions/MessengerStartFileTransfer.swift b/Sources/XXMessengerClient/Messenger/Functions/MessengerStartFileTransfer.swift
new file mode 100644
index 0000000000000000000000000000000000000000..11dfd35a4cca4cb15e7fb3d3e8f27b85501b322f
--- /dev/null
+++ b/Sources/XXMessengerClient/Messenger/Functions/MessengerStartFileTransfer.swift
@@ -0,0 +1,39 @@
+import XCTestDynamicOverlay
+import XXClient
+
+public struct MessengerStartFileTransfer {
+  public enum Error: Swift.Error, Equatable {
+    case notConnected
+  }
+
+  public var run: () throws -> Void
+
+  public func callAsFunction() throws -> Void {
+    try run()
+  }
+}
+
+extension MessengerStartFileTransfer {
+  public static func live(_ env: MessengerEnvironment) -> MessengerStartFileTransfer {
+    MessengerStartFileTransfer {
+      guard let e2e = env.e2e() else {
+        throw Error.notConnected
+      }
+      let fileTransfer = try env.initFileTransfer(
+        params: InitFileTransfer.Params(
+          e2eId: e2e.getId(),
+          e2eFileTransferParamsJSON: env.getE2EFileTransferParams(),
+          fileTransferParamsJSON: env.getFileTransferParams()
+        ),
+        callback: env.receiveFileCallbacks.registered()
+      )
+      env.fileTransfer.set(fileTransfer)
+    }
+  }
+}
+
+extension MessengerStartFileTransfer {
+  public static let unimplemented = MessengerStartFileTransfer(
+    run: XCTUnimplemented("\(Self.self)")
+  )
+}
diff --git a/Sources/XXMessengerClient/Messenger/Messenger.swift b/Sources/XXMessengerClient/Messenger/Messenger.swift
index dc427859d3b5e765e047112a9c2fd7f344f09fac..d565ebb2b6515f00bacd776be88f83f7bcd7401c 100644
--- a/Sources/XXMessengerClient/Messenger/Messenger.swift
+++ b/Sources/XXMessengerClient/Messenger/Messenger.swift
@@ -40,7 +40,10 @@ public struct Messenger {
   public var stopBackup: MessengerStopBackup
   public var setLogLevel: MessengerSetLogLevel
   public var startLogging: MessengerStartLogging
+  public var registerReceiveFileCallback: MessengerRegisterReceiveFileCallback
+  public var startFileTransfer: MessengerStartFileTransfer
   public var sendFile: MessengerSendFile
+  public var receiveFile: MessengerReceiveFile
 }
 
 extension Messenger {
@@ -85,7 +88,10 @@ extension Messenger {
       stopBackup: .live(env),
       setLogLevel: .live(env),
       startLogging: .live(env),
-      sendFile: .live(env)
+      registerReceiveFileCallback: .live(env),
+      startFileTransfer: .live(env),
+      sendFile: .live(env),
+      receiveFile: .live(env)
     )
   }
 }
@@ -131,6 +137,9 @@ extension Messenger {
     stopBackup: .unimplemented,
     setLogLevel: .unimplemented,
     startLogging: .unimplemented,
-    sendFile: .unimplemented
+    registerReceiveFileCallback: .unimplemented,
+    startFileTransfer: .unimplemented,
+    sendFile: .unimplemented,
+    receiveFile: .unimplemented
   )
 }
diff --git a/Sources/XXMessengerClient/Messenger/MessengerEnvironment.swift b/Sources/XXMessengerClient/Messenger/MessengerEnvironment.swift
index 0bd1212cebf2bf1d373d1733d2506e1c52b3d098..a1ab1517817248c93ce78bb435ceb621d5449fa6 100644
--- a/Sources/XXMessengerClient/Messenger/MessengerEnvironment.swift
+++ b/Sources/XXMessengerClient/Messenger/MessengerEnvironment.swift
@@ -10,6 +10,7 @@ public struct MessengerEnvironment {
   public var downloadNDF: DownloadAndVerifySignedNdf
   public var e2e: Stored<E2E?>
   public var fileManager: MessengerFileManager
+  public var fileTransfer: Stored<FileTransfer?>
   public var generateSecret: GenerateSecret
   public var getCMixParams: GetCMixParams
   public var getE2EFileTransferParams: GetE2EFileTransferParams
@@ -32,6 +33,7 @@ public struct MessengerEnvironment {
   public var newOrLoadUd: NewOrLoadUd
   public var newUdManagerFromBackup: NewUdManagerFromBackup
   public var passwordStorage: PasswordStorage
+  public var receiveFileCallbacks: ReceiveFileCallbacksRegistry
   public var registerForNotifications: RegisterForNotifications
   public var registerLogWriter: RegisterLogWriter
   public var resumeBackup: ResumeBackup
@@ -59,6 +61,7 @@ extension MessengerEnvironment {
       downloadNDF: .live,
       e2e: .inMemory(),
       fileManager: .live(),
+      fileTransfer: .inMemory(),
       generateSecret: .live,
       getCMixParams: .liveDefault,
       getE2EFileTransferParams: .liveDefault,
@@ -81,6 +84,7 @@ extension MessengerEnvironment {
       newOrLoadUd: .live,
       newUdManagerFromBackup: .live,
       passwordStorage: .keychain,
+      receiveFileCallbacks: .live(),
       registerForNotifications: .live,
       registerLogWriter: .live,
       resumeBackup: .live,
@@ -103,6 +107,7 @@ extension MessengerEnvironment {
     downloadNDF: .unimplemented,
     e2e: .unimplemented(),
     fileManager: .unimplemented,
+    fileTransfer: .unimplemented(),
     generateSecret: .unimplemented,
     getCMixParams: .unimplemented,
     getE2EFileTransferParams: .unimplemented,
@@ -125,6 +130,7 @@ extension MessengerEnvironment {
     newOrLoadUd: .unimplemented,
     newUdManagerFromBackup: .unimplemented,
     passwordStorage: .unimplemented,
+    receiveFileCallbacks: .unimplemented,
     registerForNotifications: .unimplemented,
     registerLogWriter: .unimplemented,
     resumeBackup: .unimplemented,
diff --git a/Sources/XXMessengerClient/Utils/ReceiveFileCallbacksRegistry.swift b/Sources/XXMessengerClient/Utils/ReceiveFileCallbacksRegistry.swift
new file mode 100644
index 0000000000000000000000000000000000000000..e82670ca748e4d28469d3b5c66b5eae0f77e4903
--- /dev/null
+++ b/Sources/XXMessengerClient/Utils/ReceiveFileCallbacksRegistry.swift
@@ -0,0 +1,36 @@
+import Foundation
+import XCTestDynamicOverlay
+import XXClient
+
+public struct ReceiveFileCallbacksRegistry {
+  public var register: (ReceiveFileCallback) -> Cancellable
+  public var registered: () -> ReceiveFileCallback
+}
+
+extension ReceiveFileCallbacksRegistry {
+  public static func live() -> ReceiveFileCallbacksRegistry {
+    class Registry {
+      var callbacks: [UUID: ReceiveFileCallback] = [:]
+    }
+    let registry = Registry()
+    return ReceiveFileCallbacksRegistry(
+      register: { callback in
+        let id = UUID()
+        registry.callbacks[id] = callback
+        return Cancellable { registry.callbacks[id] = nil }
+      },
+      registered: {
+        ReceiveFileCallback { result in
+          registry.callbacks.values.forEach { $0.handle(result) }
+        }
+      }
+    )
+  }
+}
+
+extension ReceiveFileCallbacksRegistry {
+  public static let unimplemented = ReceiveFileCallbacksRegistry(
+    register: XCTUnimplemented("\(Self.self).register", placeholder: Cancellable {}),
+    registered: XCTUnimplemented("\(Self.self).registered", placeholder: .unimplemented)
+  )
+}
diff --git a/Tests/XXMessengerClientTests/Messenger/Functions/MessengerReceiveFileTests.swift b/Tests/XXMessengerClientTests/Messenger/Functions/MessengerReceiveFileTests.swift
new file mode 100644
index 0000000000000000000000000000000000000000..554b7f309114fbdaa2ea2679d290d09f238eaa52
--- /dev/null
+++ b/Tests/XXMessengerClientTests/Messenger/Functions/MessengerReceiveFileTests.swift
@@ -0,0 +1,223 @@
+import CustomDump
+import XCTest
+import XXClient
+@testable import XXMessengerClient
+
+final class MessengerReceiveFileTests: XCTestCase {
+  func testReceiveFile() throws {
+    let params: MessengerReceiveFile.Params = .stub
+    let receivedData = "received".data(using: .utf8)!
+
+    var didRegisterReceivedProgressCallbackWithTransferId: [Data] = []
+    var didRegisterReceivedProgressCallbackWithPeriod: [Int] = []
+    var didRegisterReceivedProgressCallbackWithCallback: [FileTransferProgressCallback] = []
+    var didReceiveTransferId: [Data] = []
+    var didReceiveCallback: [MessengerReceiveFile.CallbackInfo] = []
+
+    var env: MessengerEnvironment = .unimplemented
+    env.fileTransfer.get = {
+      var fileTransfer: FileTransfer = .unimplemented
+      fileTransfer.registerReceivedProgressCallback.run = { transferId, period, callback in
+        didRegisterReceivedProgressCallbackWithTransferId.append(transferId)
+        didRegisterReceivedProgressCallbackWithPeriod.append(period)
+        didRegisterReceivedProgressCallbackWithCallback.append(callback)
+      }
+      fileTransfer.receive.run = { transferId in
+        didReceiveTransferId.append(transferId)
+        return receivedData
+      }
+      return fileTransfer
+    }
+    let receiveFile: MessengerReceiveFile = .live(env)
+
+    try receiveFile(params) { info in
+      didReceiveCallback.append(info)
+    }
+
+    XCTAssertNoDifference(didRegisterReceivedProgressCallbackWithTransferId, [
+      params.transferId
+    ])
+    XCTAssertNoDifference(didRegisterReceivedProgressCallbackWithPeriod, [
+      params.callbackIntervalMS
+    ])
+    XCTAssertNoDifference(didReceiveCallback, [])
+
+    didReceiveCallback = []
+    didRegisterReceivedProgressCallbackWithCallback.first?.handle(.success(
+      FileTransferProgressCallback.Callback(
+        progress: Progress(
+          completed: false,
+          transmitted: 1,
+          total: 3,
+          error: nil
+        ),
+        partTracker: .unimplemented
+      )
+    ))
+
+    XCTAssertNoDifference(didReceiveCallback, [
+      .progress(transmitted: 1, total: 3),
+    ])
+
+    didReceiveCallback = []
+    didRegisterReceivedProgressCallbackWithCallback.first?.handle(.success(
+      FileTransferProgressCallback.Callback(
+        progress: Progress(
+          completed: false,
+          transmitted: 2,
+          total: 3,
+          error: nil
+        ),
+        partTracker: .unimplemented
+      )
+    ))
+
+    XCTAssertNoDifference(didReceiveCallback, [
+      .progress(transmitted: 2, total: 3),
+    ])
+
+    didReceiveCallback = []
+    didRegisterReceivedProgressCallbackWithCallback.first?.handle(.success(
+      FileTransferProgressCallback.Callback(
+        progress: Progress(
+          completed: true,
+          transmitted: 3,
+          total: 3,
+          error: nil
+        ),
+        partTracker: .unimplemented
+      )
+    ))
+
+    XCTAssertNoDifference(didReceiveTransferId, [
+      params.transferId,
+    ])
+    XCTAssertNoDifference(didReceiveCallback, [
+      .finished(receivedData),
+    ])
+  }
+
+  func testReceiveFileWhenNotConnected() {
+    var env: MessengerEnvironment = .unimplemented
+    env.fileTransfer.get = { nil }
+    let receiveFile: MessengerReceiveFile = .live(env)
+
+    XCTAssertThrowsError(try receiveFile(.stub) { _ in }) { error in
+      XCTAssertNoDifference(
+        error as? MessengerReceiveFile.Error,
+        MessengerReceiveFile.Error.fileTransferNotStarted
+      )
+    }
+  }
+
+  func testReceiveFileProgressError() throws {
+    let error = "Something went wrong..."
+
+    var receivedProgressCallback: FileTransferProgressCallback?
+    var didReceiveCallback: [MessengerReceiveFile.CallbackInfo] = []
+
+    var env: MessengerEnvironment = .unimplemented
+    env.fileTransfer.get = {
+      var fileTransfer: FileTransfer = .unimplemented
+      fileTransfer.registerReceivedProgressCallback.run = { _, _, callback in
+        receivedProgressCallback = callback
+      }
+      return fileTransfer
+    }
+    let receiveFile: MessengerReceiveFile = .live(env)
+
+    try receiveFile(.stub) { info in
+      didReceiveCallback.append(info)
+    }
+
+    receivedProgressCallback?.handle(.success(
+      FileTransferProgressCallback.Callback(
+        progress: Progress(
+          completed: false,
+          transmitted: 1,
+          total: 3,
+          error: error
+        ),
+        partTracker: .unimplemented
+      )
+    ))
+
+    XCTAssertNoDifference(didReceiveCallback, [
+      .failed(.progressError(error))
+    ])
+  }
+
+  func testReceiveFileCallbackError() throws {
+    let error = NSError(domain: "test", code: 123)
+
+    var receivedProgressCallback: FileTransferProgressCallback?
+    var didReceiveCallback: [MessengerReceiveFile.CallbackInfo] = []
+
+    var env: MessengerEnvironment = .unimplemented
+    env.fileTransfer.get = {
+      var fileTransfer: FileTransfer = .unimplemented
+      fileTransfer.registerReceivedProgressCallback.run = { _, _, callback in
+        receivedProgressCallback = callback
+      }
+      return fileTransfer
+    }
+    let receiveFile: MessengerReceiveFile = .live(env)
+
+    try receiveFile(.stub) { info in
+      didReceiveCallback.append(info)
+    }
+
+    receivedProgressCallback?.handle(.failure(error))
+
+    XCTAssertNoDifference(didReceiveCallback, [
+      .failed(.callbackError(error))
+    ])
+  }
+
+  func testReceiveFileReceiveError() throws {
+    let error = NSError(domain: "test", code: 123)
+
+    var receivedProgressCallback: FileTransferProgressCallback?
+    var didReceiveCallback: [MessengerReceiveFile.CallbackInfo] = []
+
+    var env: MessengerEnvironment = .unimplemented
+    env.fileTransfer.get = {
+      var fileTransfer: FileTransfer = .unimplemented
+      fileTransfer.registerReceivedProgressCallback.run = { _, _, callback in
+        receivedProgressCallback = callback
+      }
+      fileTransfer.receive.run = { _ in
+        throw error
+      }
+      return fileTransfer
+    }
+    let receiveFile: MessengerReceiveFile = .live(env)
+
+    try receiveFile(.stub) { info in
+      didReceiveCallback.append(info)
+    }
+
+    receivedProgressCallback?.handle(.success(
+      FileTransferProgressCallback.Callback(
+        progress: Progress(
+          completed: true,
+          transmitted: 3,
+          total: 3,
+          error: nil
+        ),
+        partTracker: .unimplemented
+      )
+    ))
+
+    XCTAssertNoDifference(didReceiveCallback, [
+      .failed(.receiveError(error))
+    ])
+  }
+}
+
+private extension MessengerReceiveFile.Params {
+  static let stub = MessengerReceiveFile.Params(
+    transferId: "transfer-id".data(using: .utf8)!,
+    callbackIntervalMS: 123
+  )
+}
diff --git a/Tests/XXMessengerClientTests/Messenger/Functions/MessengerRegisterReceiveFileCallbackTests.swift b/Tests/XXMessengerClientTests/Messenger/Functions/MessengerRegisterReceiveFileCallbackTests.swift
new file mode 100644
index 0000000000000000000000000000000000000000..95a6cabcacb1f53f30bf5c25b26f6843928e9034
--- /dev/null
+++ b/Tests/XXMessengerClientTests/Messenger/Functions/MessengerRegisterReceiveFileCallbackTests.swift
@@ -0,0 +1,34 @@
+import CustomDump
+import XCTest
+import XXClient
+@testable import XXMessengerClient
+
+final class MessengerRegisterReceiveFileCallbackTests: XCTestCase {
+  func testRegisterCallback() {
+    var registeredCallbacks: [ReceiveFileCallback] = []
+    var didHandleResult: [ReceiveFileCallback.Result] = []
+    var didCancelRegisteredCallback = 0
+
+    var env: MessengerEnvironment = .unimplemented
+    env.receiveFileCallbacks.register = { callback in
+      registeredCallbacks.append(callback)
+      return Cancellable { didCancelRegisteredCallback += 1 }
+    }
+    let registerCallback: MessengerRegisterReceiveFileCallback = .live(env)
+    let cancellable = registerCallback(ReceiveFileCallback { result in
+      didHandleResult.append(result)
+    })
+
+    XCTAssertEqual(registeredCallbacks.count, 1)
+
+    registeredCallbacks.forEach { callback in
+      callback.handle(.success(.stub(1)))
+    }
+
+    XCTAssertNoDifference(didHandleResult, [.success(.stub(1))])
+
+    cancellable.cancel()
+
+    XCTAssertEqual(didCancelRegisteredCallback, 1)
+  }
+}
diff --git a/Tests/XXMessengerClientTests/Messenger/Functions/MessengerSendFileTests.swift b/Tests/XXMessengerClientTests/Messenger/Functions/MessengerSendFileTests.swift
index 9adc15b1d89fe2f19445629fb1381b5520661233..4e1c1763b489e66206ca18668bd673d8d75c5570 100644
--- a/Tests/XXMessengerClientTests/Messenger/Functions/MessengerSendFileTests.swift
+++ b/Tests/XXMessengerClientTests/Messenger/Functions/MessengerSendFileTests.swift
@@ -5,28 +5,15 @@ import XXClient
 
 final class MessengerSendFileTests: XCTestCase {
   func testSendFile() throws {
-    let e2eId = 123
-    let e2eFileTransferParams = "e2eFileTransferParams".data(using: .utf8)!
-    let fileTransferParams = "fileTransferParams".data(using: .utf8)!
     let newTransferId = "transferId".data(using: .utf8)!
 
-    var didInitFileTransfer: [InitFileTransfer.Params] = []
     var didSendFile: [FileTransferSend.Params] = []
     var didCloseSend: [Data] = []
     var didReceiveCallback: [MessengerSendFile.CallbackInfo] = []
-
     var fileTransferProgressCallback: FileTransferProgressCallback!
 
     var env: MessengerEnvironment = .unimplemented
-    env.e2e.get = {
-      var e2e: E2E = .unimplemented
-      e2e.getId.run = { e2eId }
-      return e2e
-    }
-    env.getE2EFileTransferParams.run = { e2eFileTransferParams }
-    env.getFileTransferParams.run = { fileTransferParams }
-    env.initFileTransfer.run = { params, callback in
-      didInitFileTransfer.append(params)
+    env.fileTransfer.get = {
       var fileTransfer: FileTransfer = .unimplemented
       fileTransfer.send.run = { params, callback in
         didSendFile.append(params)
@@ -47,13 +34,6 @@ final class MessengerSendFileTests: XCTestCase {
     }
 
     XCTAssertNoDifference(transferId, newTransferId)
-    XCTAssertNoDifference(didInitFileTransfer, [
-      .init(
-        e2eId: e2eId,
-        e2eFileTransferParamsJSON: e2eFileTransferParams,
-        fileTransferParamsJSON: fileTransferParams
-      )
-    ])
     XCTAssertNoDifference(didSendFile, [
       .init(
         payload: params.file,
@@ -99,15 +79,15 @@ final class MessengerSendFileTests: XCTestCase {
     XCTAssertNoDifference(didCloseSend, [transferId])
   }
 
-  func testSendFileWhenNotConnected() {
+  func testSendFileWhenNotStarted() {
     var env: MessengerEnvironment = .unimplemented
-    env.e2e.get = { nil }
+    env.fileTransfer.get = { nil }
     let sendFile: MessengerSendFile = .live(env)
 
     XCTAssertThrowsError(try sendFile(.stub) { _ in }) { error in
       XCTAssertNoDifference(
         error as? MessengerSendFile.Error,
-        MessengerSendFile.Error.notConnected
+        MessengerSendFile.Error.fileTransferNotStarted
       )
     }
   }
@@ -118,14 +98,7 @@ final class MessengerSendFileTests: XCTestCase {
     var fileTransferProgressCallback: FileTransferProgressCallback!
 
     var env: MessengerEnvironment = .unimplemented
-    env.e2e.get = {
-      var e2e: E2E = .unimplemented
-      e2e.getId.run = { 123 }
-      return e2e
-    }
-    env.getE2EFileTransferParams.run = { Data() }
-    env.getFileTransferParams.run = { Data() }
-    env.initFileTransfer.run = { params, callback in
+    env.fileTransfer.get = {
       var fileTransfer: FileTransfer = .unimplemented
       fileTransfer.send.run = { _, callback in
         fileTransferProgressCallback = callback
@@ -157,14 +130,7 @@ final class MessengerSendFileTests: XCTestCase {
     var fileTransferProgressCallback: FileTransferProgressCallback!
 
     var env: MessengerEnvironment = .unimplemented
-    env.e2e.get = {
-      var e2e: E2E = .unimplemented
-      e2e.getId.run = { 123 }
-      return e2e
-    }
-    env.getE2EFileTransferParams.run = { Data() }
-    env.getFileTransferParams.run = { Data() }
-    env.initFileTransfer.run = { params, callback in
+    env.fileTransfer.get = {
       var fileTransfer: FileTransfer = .unimplemented
       fileTransfer.send.run = { _, callback in
         fileTransferProgressCallback = callback
@@ -205,14 +171,7 @@ final class MessengerSendFileTests: XCTestCase {
     var fileTransferProgressCallback: FileTransferProgressCallback!
 
     var env: MessengerEnvironment = .unimplemented
-    env.e2e.get = {
-      var e2e: E2E = .unimplemented
-      e2e.getId.run = { 123 }
-      return e2e
-    }
-    env.getE2EFileTransferParams.run = { Data() }
-    env.getFileTransferParams.run = { Data() }
-    env.initFileTransfer.run = { params, callback in
+    env.fileTransfer.get = {
       var fileTransfer: FileTransfer = .unimplemented
       fileTransfer.send.run = { _, callback in
         fileTransferProgressCallback = callback
diff --git a/Tests/XXMessengerClientTests/Messenger/Functions/MessengerStartFileTransferTests.swift b/Tests/XXMessengerClientTests/Messenger/Functions/MessengerStartFileTransferTests.swift
new file mode 100644
index 0000000000000000000000000000000000000000..8e6d352880f7c2bbd27700f79fa0cef54c120cf1
--- /dev/null
+++ b/Tests/XXMessengerClientTests/Messenger/Functions/MessengerStartFileTransferTests.swift
@@ -0,0 +1,80 @@
+import CustomDump
+import XCTest
+import XXClient
+@testable import XXMessengerClient
+
+final class MessengerStartFileTransferTests: XCTestCase {
+  func testStart() throws {
+    let e2eId = 123
+    let e2eFileTransferParams = "e2eFileTransferParams".data(using: .utf8)!
+    let fileTransferParams = "fileTransferParams".data(using: .utf8)!
+
+    var didInitFileTransfer: [InitFileTransfer.Params] = []
+    var receiveFileCallback: ReceiveFileCallback?
+    var didSetFileTransfer: [FileTransfer?] = []
+    var didReceiveFile: [ReceiveFileCallback.Result] = []
+
+    var env: MessengerEnvironment = .unimplemented
+    env.e2e.get = {
+      var e2e: E2E = .unimplemented
+      e2e.getId.run = { e2eId }
+      return e2e
+    }
+    env.getE2EFileTransferParams.run = {
+      e2eFileTransferParams
+    }
+    env.getFileTransferParams.run = {
+      fileTransferParams
+    }
+    env.initFileTransfer.run = { params, callback in
+      didInitFileTransfer.append(params)
+      receiveFileCallback = callback
+      return .unimplemented
+    }
+    env.fileTransfer.set = {
+      didSetFileTransfer.append($0)
+    }
+    env.receiveFileCallbacks.registered = {
+      ReceiveFileCallback { result in
+        didReceiveFile.append(result)
+      }
+    }
+
+    let start: MessengerStartFileTransfer = .live(env)
+
+    try start()
+
+    XCTAssertNoDifference(didInitFileTransfer, [.init(
+      e2eId: e2eId,
+      e2eFileTransferParamsJSON: e2eFileTransferParams,
+      fileTransferParamsJSON: fileTransferParams
+    )])
+    XCTAssertNotNil(receiveFileCallback)
+    XCTAssertNoDifference(didSetFileTransfer.map { $0 != nil }, [true])
+
+    let error = NSError(domain: "test", code: 7)
+    receiveFileCallback?.handle(.success(.stub(1)))
+    receiveFileCallback?.handle(.failure(error))
+    receiveFileCallback?.handle(.success(.stub(2)))
+
+    XCTAssertNoDifference(didReceiveFile, [
+      .success(.stub(1)),
+      .failure(error),
+      .success(.stub(2)),
+    ])
+  }
+
+  func testStartWhenNotConnected() {
+    var env: MessengerEnvironment = .unimplemented
+    env.e2e.get = { nil }
+    let start: MessengerStartFileTransfer = .live(env)
+
+    XCTAssertThrowsError(try start()) { error in
+      XCTAssertNoDifference(
+        error as NSError,
+        MessengerStartFileTransfer.Error.notConnected as NSError
+      )
+    }
+  }
+}
+
diff --git a/Tests/XXMessengerClientTests/TestHelpers/TestDoubles.swift b/Tests/XXMessengerClientTests/TestHelpers/TestDoubles.swift
index c9a90bde352a9da4ac0c359c26fed8e1a89d9f8f..93110b2efb3698aec563f2f6cc9a0e51f03189a9 100644
--- a/Tests/XXMessengerClientTests/TestHelpers/TestDoubles.swift
+++ b/Tests/XXMessengerClientTests/TestHelpers/TestDoubles.swift
@@ -23,3 +23,16 @@ extension BackupParams {
     username: "stub-username"
   )
 }
+
+extension ReceivedFile {
+  static func stub(_ id: Int) -> ReceivedFile {
+    ReceivedFile(
+      transferId: "transfer-id-\(id)".data(using: .utf8)!,
+      senderId: "sender-id-\(id)".data(using: .utf8)!,
+      preview: "preview-\(id)".data(using: .utf8)!,
+      name: "name-\(id)",
+      type: "type-\(id)",
+      size: id
+    )
+  }
+}
diff --git a/Tests/XXMessengerClientTests/Utils/ReceiveFileCallbacksRegistryTests.swift b/Tests/XXMessengerClientTests/Utils/ReceiveFileCallbacksRegistryTests.swift
new file mode 100644
index 0000000000000000000000000000000000000000..87733caac8992f8eebfb8ee52497eb79437745be
--- /dev/null
+++ b/Tests/XXMessengerClientTests/Utils/ReceiveFileCallbacksRegistryTests.swift
@@ -0,0 +1,44 @@
+import CustomDump
+import XCTest
+import XXClient
+@testable import XXMessengerClient
+
+final class ReceiveFileCallbacksRegistryTests: XCTestCase {
+  func testRegistry() {
+    var firstCallbackDidHandle: [ReceiveFileCallback.Result] = []
+    var secondCallbackDidHandle: [ReceiveFileCallback.Result] = []
+
+    let firstCallback = ReceiveFileCallback { result in
+      firstCallbackDidHandle.append(result)
+    }
+    let secondCallback = ReceiveFileCallback { result in
+      secondCallbackDidHandle.append(result)
+    }
+    let callbackRegistry: ReceiveFileCallbacksRegistry = .live()
+    let registeredCallbacks = callbackRegistry.registered()
+    let firstCallbackCancellable = callbackRegistry.register(firstCallback)
+    let secondCallbackCancellable = callbackRegistry.register(secondCallback)
+
+    let firstResult = ReceiveFileCallback.Result.success(.stub(1))
+    registeredCallbacks.handle(firstResult)
+
+    XCTAssertNoDifference(firstCallbackDidHandle, [firstResult])
+    XCTAssertNoDifference(secondCallbackDidHandle, [firstResult])
+
+    firstCallbackCancellable.cancel()
+    let secondError = NSError(domain: "test", code: 321)
+    let secondResult = ReceiveFileCallback.Result.failure(secondError)
+    registeredCallbacks.handle(secondResult)
+
+    XCTAssertNoDifference(firstCallbackDidHandle, [firstResult])
+    XCTAssertNoDifference(secondCallbackDidHandle, [firstResult, secondResult])
+
+    secondCallbackCancellable.cancel()
+
+    let thirdData = ReceiveFileCallback.Result.success(.stub(2))
+    registeredCallbacks.handle(thirdData)
+
+    XCTAssertNoDifference(firstCallbackDidHandle, [firstResult])
+    XCTAssertNoDifference(secondCallbackDidHandle, [firstResult, secondResult])
+  }
+}