diff --git a/Sources/XXClient/Callbacks/FileTransferProgressCallback.swift b/Sources/XXClient/Callbacks/FileTransferProgressCallback.swift
index faf5a70081d9c96f4e05ba4bfc19598e1fd635b6..9c77d7af95ef9f47c42ff47ec682af4658b9ee50 100644
--- a/Sources/XXClient/Callbacks/FileTransferProgressCallback.swift
+++ b/Sources/XXClient/Callbacks/FileTransferProgressCallback.swift
@@ -3,6 +3,14 @@ import XCTestDynamicOverlay
 
 public struct FileTransferProgressCallback {
   public struct Callback {
+    public init(
+      progress: Progress,
+      partTracker: FilePartTracker
+    ) {
+      self.progress = progress
+      self.partTracker = partTracker
+    }
+
     public var progress: Progress
     public var partTracker: FilePartTracker
   }
diff --git a/Sources/XXClient/Functions/InitFileTransfer.swift b/Sources/XXClient/Functions/InitFileTransfer.swift
index e232ef746ee5c4fcae42afe14a22e715e4a84687..f56b5680e030aeec708bcfee62e5cd38eaa78d72 100644
--- a/Sources/XXClient/Functions/InitFileTransfer.swift
+++ b/Sources/XXClient/Functions/InitFileTransfer.swift
@@ -2,28 +2,40 @@ import Bindings
 import XCTestDynamicOverlay
 
 public struct InitFileTransfer {
-  public var run: (Int, Data, Data, ReceiveFileCallback) throws -> FileTransfer
+  public struct Params: Equatable {
+    public init(
+      e2eId: Int,
+      e2eFileTransferParamsJSON: Data = GetE2EFileTransferParams.liveDefault(),
+      fileTransferParamsJSON: Data = GetFileTransferParams.liveDefault()
+    ) {
+      self.e2eId = e2eId
+      self.e2eFileTransferParamsJSON = e2eFileTransferParamsJSON
+      self.fileTransferParamsJSON = fileTransferParamsJSON
+    }
+
+    public var e2eId: Int
+    public var e2eFileTransferParamsJSON: Data = GetE2EFileTransferParams.liveDefault()
+    public var fileTransferParamsJSON: Data = GetFileTransferParams.liveDefault()
+  }
+
+  public var run: (Params, ReceiveFileCallback) throws -> FileTransfer
 
   public func callAsFunction(
-    e2eId: Int,
-    e2eFileTransferParamsJSON: Data = GetE2EFileTransferParams.liveDefault(),
-    fileTransferParamsJSON: Data = GetFileTransferParams.liveDefault(),
+    params: Params,
     callback: ReceiveFileCallback
   ) throws -> FileTransfer {
-    try run(e2eId, e2eFileTransferParamsJSON, fileTransferParamsJSON, callback)
+    try run(params, callback)
   }
 }
 
 extension InitFileTransfer {
-  public static let live = InitFileTransfer {
-    e2eId, e2eFileTransferParamsJSON, fileTransferParamsJSON, callback in
-
+  public static let live = InitFileTransfer { params, callback in
     var error: NSError?
     let bindingsFileTransfer = BindingsInitFileTransfer(
-      e2eId,
+      params.e2eId,
       callback.makeBindingsReceiveFileCallback(),
-      e2eFileTransferParamsJSON,
-      fileTransferParamsJSON,
+      params.e2eFileTransferParamsJSON,
+      params.fileTransferParamsJSON,
       &error
     )
     if let error = error {
diff --git a/Sources/XXMessengerClient/Messenger/Functions/MessengerSendFile.swift b/Sources/XXMessengerClient/Messenger/Functions/MessengerSendFile.swift
new file mode 100644
index 0000000000000000000000000000000000000000..8aca65c4a59f7d55d350f5ee8c9b189d1217fbe5
--- /dev/null
+++ b/Sources/XXMessengerClient/Messenger/Functions/MessengerSendFile.swift
@@ -0,0 +1,119 @@
+import Foundation
+import XCTestDynamicOverlay
+import XXClient
+
+public struct MessengerSendFile {
+  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: (Params, @escaping Callback) throws -> Data
+
+  public func callAsFunction(
+    _ params: Params,
+    callback: @escaping Callback
+  ) throws -> Data {
+    try run(params, callback)
+  }
+}
+
+extension MessengerSendFile {
+  public static func live(_ env: MessengerEnvironment) -> MessengerSendFile {
+    MessengerSendFile { params, callback in
+      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: ReceiveFileCallback { _ in
+          fatalError("Bindings issue: ReceiveFileCallback called when sending file.")
+        }
+      )
+      func close(id: Data) {
+        do {
+          try fileTransfer.closeSend(transferId: id)
+        } catch {
+          callback(.failed(id: id, .close(error as NSError)))
+        }
+      }
+      var transferId: Data!
+      transferId = try fileTransfer.send(
+        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 error):
+            callback(.failed(id: transferId, .error(error)))
+            close(id: transferId)
+
+          case .success(let cb):
+            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
+              ))
+            }
+          }
+        }
+      )
+      return transferId
+    }
+  }
+}
+
+extension MessengerSendFile {
+  public static let unimplemented = MessengerSendFile(
+    run: XCTUnimplemented("\(Self.self)")
+  )
+}
diff --git a/Sources/XXMessengerClient/Messenger/MessengerEnvironment.swift b/Sources/XXMessengerClient/Messenger/MessengerEnvironment.swift
index 52af520c6df5dc21e600d9086a85a36b1d640d68..0bd1212cebf2bf1d373d1733d2506e1c52b3d098 100644
--- a/Sources/XXMessengerClient/Messenger/MessengerEnvironment.swift
+++ b/Sources/XXMessengerClient/Messenger/MessengerEnvironment.swift
@@ -12,8 +12,11 @@ public struct MessengerEnvironment {
   public var fileManager: MessengerFileManager
   public var generateSecret: GenerateSecret
   public var getCMixParams: GetCMixParams
+  public var getE2EFileTransferParams: GetE2EFileTransferParams
   public var getE2EParams: GetE2EParams
+  public var getFileTransferParams: GetFileTransferParams
   public var getSingleUseParams: GetSingleUseParams
+  public var initFileTransfer: InitFileTransfer
   public var initializeBackup: InitializeBackup
   public var isListeningForMessages: Stored<Bool>
   public var isRegisteredWithUD: IsRegisteredWithUD
@@ -58,8 +61,11 @@ extension MessengerEnvironment {
       fileManager: .live(),
       generateSecret: .live,
       getCMixParams: .liveDefault,
+      getE2EFileTransferParams: .liveDefault,
       getE2EParams: .liveDefault,
+      getFileTransferParams: .liveDefault,
       getSingleUseParams: .liveDefault,
+      initFileTransfer: .live,
       initializeBackup: .live,
       isListeningForMessages: .inMemory(false),
       isRegisteredWithUD: .live,
@@ -99,8 +105,11 @@ extension MessengerEnvironment {
     fileManager: .unimplemented,
     generateSecret: .unimplemented,
     getCMixParams: .unimplemented,
+    getE2EFileTransferParams: .unimplemented,
     getE2EParams: .unimplemented,
+    getFileTransferParams: .unimplemented,
     getSingleUseParams: .unimplemented,
+    initFileTransfer: .unimplemented,
     initializeBackup: .unimplemented,
     isListeningForMessages: .unimplemented(placeholder: false),
     isRegisteredWithUD: .unimplemented,
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
+  )
+}