diff --git a/Sources/XXMessengerClient/Messenger/Functions/MessengerSendFile.swift b/Sources/XXMessengerClient/Messenger/Functions/MessengerSendFile.swift
index 4732eccf5d7792700211c1a9d152f280f89d9584..8aca65c4a59f7d55d350f5ee8c9b189d1217fbe5 100644
--- a/Sources/XXMessengerClient/Messenger/Functions/MessengerSendFile.swift
+++ b/Sources/XXMessengerClient/Messenger/Functions/MessengerSendFile.swift
@@ -3,18 +3,49 @@ import XCTestDynamicOverlay
 import XXClient
 
 public struct MessengerSendFile {
-  public typealias Callback = (Data, XXClient.Progress) -> Void
+  public struct Params: Equatable {
+    public init(
+      file: FileSend,
+      recipientId: Data,
+      retry: Int = 3,
+      callbackIntervalMS: Int = 250
+    ) {
+      self.file = file
+      self.recipientId = recipientId
+      self.retry = retry
+      self.callbackIntervalMS = callbackIntervalMS
+    }
+
+    public var file: FileSend
+    public var recipientId: Data
+    public var retry: Int
+    public var callbackIntervalMS: Int
+  }
+
+  public enum CallbackInfo: Equatable {
+    public enum Failure: Equatable {
+      case error(NSError)
+      case progressError(String)
+      case close(NSError)
+    }
+
+    case progress(id: Data, transmitted: Int, total: Int)
+    case finished(id: Data)
+    case failed(id: Data, Failure)
+  }
+
+  public typealias Callback = (CallbackInfo) -> Void
 
   public enum Error: Swift.Error, Equatable {
     case notConnected
   }
 
-  public var run: (FileTransferSend.Params, @escaping Callback) throws -> Void
+  public var run: (Params, @escaping Callback) throws -> Data
 
   public func callAsFunction(
-    _ params: FileTransferSend.Params,
+    _ params: Params,
     callback: @escaping Callback
-  ) throws -> Void {
+  ) throws -> Data {
     try run(params, callback)
   }
 }
@@ -35,33 +66,48 @@ extension MessengerSendFile {
           fatalError("Bindings issue: ReceiveFileCallback called when sending file.")
         }
       )
-      let semaphore = DispatchSemaphore(value: 0)
+      func close(id: Data) {
+        do {
+          try fileTransfer.closeSend(transferId: id)
+        } catch {
+          callback(.failed(id: id, .close(error as NSError)))
+        }
+      }
       var transferId: Data!
-      var error: Swift.Error?
       transferId = try fileTransfer.send(
-        params: params,
+        params: FileTransferSend.Params(
+          payload: params.file,
+          recipientId: params.recipientId,
+          retry: Float(params.retry),
+          period: params.callbackIntervalMS
+        ),
         callback: FileTransferProgressCallback { result in
           guard let transferId else {
             fatalError("Bindings issue: file transfer progress callback was called before send function returned transfer id.")
           }
           switch result {
-          case .failure(let err):
-            error = err
-            semaphore.signal()
+          case .failure(let error):
+            callback(.failed(id: transferId, .error(error)))
+            close(id: transferId)
 
           case .success(let cb):
-            callback(transferId, cb.progress)
-            if cb.progress.completed || cb.progress.error != nil {
-              semaphore.signal()
+            if let error = cb.progress.error {
+              callback(.failed(id: transferId, .progressError(error)))
+              close(id: transferId)
+            } else if cb.progress.completed {
+              callback(.finished(id: transferId))
+              close(id: transferId)
+            } else {
+              callback(.progress(
+                id: transferId,
+                transmitted: cb.progress.transmitted,
+                total: cb.progress.total
+              ))
             }
           }
         }
       )
-      semaphore.wait()
-      try fileTransfer.closeSend(transferId: transferId)
-      if let error {
-        throw error
-      }
+      return transferId
     }
   }
 }
diff --git a/Tests/XXMessengerClientTests/Messenger/Functions/MessengerSendFileTests.swift b/Tests/XXMessengerClientTests/Messenger/Functions/MessengerSendFileTests.swift
new file mode 100644
index 0000000000000000000000000000000000000000..9adc15b1d89fe2f19445629fb1381b5520661233
--- /dev/null
+++ b/Tests/XXMessengerClientTests/Messenger/Functions/MessengerSendFileTests.swift
@@ -0,0 +1,261 @@
+import CustomDump
+import XCTest
+import XXClient
+@testable import XXMessengerClient
+
+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)
+      var fileTransfer: FileTransfer = .unimplemented
+      fileTransfer.send.run = { params, callback in
+        didSendFile.append(params)
+        fileTransferProgressCallback = callback
+        return newTransferId
+      }
+      fileTransfer.closeSend.run = { id in
+        didCloseSend.append(id)
+      }
+      return fileTransfer
+    }
+
+    let sendFile: MessengerSendFile = .live(env)
+    let params = MessengerSendFile.Params.stub
+
+    let transferId = try sendFile(params) { info in
+      didReceiveCallback.append(info)
+    }
+
+    XCTAssertNoDifference(transferId, newTransferId)
+    XCTAssertNoDifference(didInitFileTransfer, [
+      .init(
+        e2eId: e2eId,
+        e2eFileTransferParamsJSON: e2eFileTransferParams,
+        fileTransferParamsJSON: fileTransferParams
+      )
+    ])
+    XCTAssertNoDifference(didSendFile, [
+      .init(
+        payload: params.file,
+        recipientId: params.recipientId,
+        retry: Float(params.retry),
+        period: params.callbackIntervalMS
+      )
+    ])
+
+    fileTransferProgressCallback.handle(.success(.init(
+      progress: Progress(
+        completed: false,
+        transmitted: 1,
+        total: 10,
+        error: nil
+      ),
+      partTracker: .unimplemented
+    )))
+    fileTransferProgressCallback.handle(.success(.init(
+      progress: Progress(
+        completed: false,
+        transmitted: 6,
+        total: 10,
+        error: nil
+      ),
+      partTracker: .unimplemented
+    )))
+    fileTransferProgressCallback.handle(.success(.init(
+      progress: Progress(
+        completed: true,
+        transmitted: 10,
+        total: 10,
+        error: nil
+      ),
+      partTracker: .unimplemented
+    )))
+
+    XCTAssertNoDifference(didReceiveCallback, [
+      .progress(id: transferId, transmitted: 1, total: 10),
+      .progress(id: transferId, transmitted: 6, total: 10),
+      .finished(id: transferId),
+    ])
+    XCTAssertNoDifference(didCloseSend, [transferId])
+  }
+
+  func testSendFileWhenNotConnected() {
+    var env: MessengerEnvironment = .unimplemented
+    env.e2e.get = { nil }
+    let sendFile: MessengerSendFile = .live(env)
+
+    XCTAssertThrowsError(try sendFile(.stub) { _ in }) { error in
+      XCTAssertNoDifference(
+        error as? MessengerSendFile.Error,
+        MessengerSendFile.Error.notConnected
+      )
+    }
+  }
+
+  func testSendFileCallbackFailure() throws {
+    var didCloseSend: [Data] = []
+    var didReceiveCallback: [MessengerSendFile.CallbackInfo] = []
+    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
+      var fileTransfer: FileTransfer = .unimplemented
+      fileTransfer.send.run = { _, callback in
+        fileTransferProgressCallback = callback
+        return "transferId".data(using: .utf8)!
+      }
+      fileTransfer.closeSend.run = { id in
+        didCloseSend.append(id)
+      }
+      return fileTransfer
+    }
+    let sendFile: MessengerSendFile = .live(env)
+
+    let transferId = try sendFile(.stub) { info in
+      didReceiveCallback.append(info)
+    }
+
+    let error = NSError(domain: "test", code: 1234)
+    fileTransferProgressCallback.handle(.failure(error))
+
+    XCTAssertNoDifference(didReceiveCallback, [
+      .failed(id: transferId, .error(error)),
+    ])
+    XCTAssertNoDifference(didCloseSend, [transferId])
+  }
+
+  func testSendFileProgressError() throws {
+    var didCloseSend: [Data] = []
+    var didReceiveCallback: [MessengerSendFile.CallbackInfo] = []
+    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
+      var fileTransfer: FileTransfer = .unimplemented
+      fileTransfer.send.run = { _, callback in
+        fileTransferProgressCallback = callback
+        return "transferId".data(using: .utf8)!
+      }
+      fileTransfer.closeSend.run = { id in
+        didCloseSend.append(id)
+      }
+      return fileTransfer
+    }
+    let sendFile: MessengerSendFile = .live(env)
+
+    let transferId = try sendFile(.stub) { info in
+      didReceiveCallback.append(info)
+    }
+
+    let error = "something went wrong"
+    fileTransferProgressCallback.handle(.success(.init(
+      progress: .init(
+        completed: false,
+        transmitted: 0,
+        total: 0,
+        error: error
+      ),
+      partTracker: .unimplemented
+    )))
+
+    XCTAssertNoDifference(didReceiveCallback, [
+      .failed(id: transferId, .progressError(error)),
+    ])
+    XCTAssertNoDifference(didCloseSend, [transferId])
+  }
+
+  func testSendFileCloseError() throws {
+    let closeError = NSError(domain: "test", code: 1234)
+
+    var didReceiveCallback: [MessengerSendFile.CallbackInfo] = []
+    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
+      var fileTransfer: FileTransfer = .unimplemented
+      fileTransfer.send.run = { _, callback in
+        fileTransferProgressCallback = callback
+        return "transferId".data(using: .utf8)!
+      }
+      fileTransfer.closeSend.run = { id in
+        throw closeError
+      }
+      return fileTransfer
+    }
+    let sendFile: MessengerSendFile = .live(env)
+
+    let transferId = try sendFile(.stub) { info in
+      didReceiveCallback.append(info)
+    }
+
+    fileTransferProgressCallback.handle(.success(.init(
+      progress: .init(
+        completed: true,
+        transmitted: 1,
+        total: 1,
+        error: nil
+      ),
+      partTracker: .unimplemented
+    )))
+
+    XCTAssertNoDifference(didReceiveCallback, [
+      .finished(id: transferId),
+      .failed(id: transferId, .close(closeError)),
+    ])
+  }
+}
+
+private extension MessengerSendFile.Params {
+  static let stub = MessengerSendFile.Params(
+    file: FileSend(
+      name: "file-name",
+      type: "file-type",
+      preview: "file-preview".data(using: .utf8)!,
+      contents: "file-contents".data(using: .utf8)!
+    ),
+    recipientId: "recipient-id".data(using: .utf8)!,
+    retry: 123,
+    callbackIntervalMS: 321
+  )
+}