diff --git a/lib/actions/upload-actions.js b/lib/actions/upload-actions.js
--- a/lib/actions/upload-actions.js
+++ b/lib/actions/upload-actions.js
@@ -1,18 +1,31 @@
 // @flow
 
+import * as uuid from 'uuid';
+
+import blobService from '../facts/blob-service.js';
 import type { Shape } from '../types/core.js';
 import type {
   UploadMediaMetadataRequest,
   UploadMultimediaResult,
   Dimensions,
 } from '../types/media-types';
+import { toBase64URL } from '../utils/base64.js';
+import {
+  blobServiceUploadHandler,
+  type BlobServiceUploadHandler,
+} from '../utils/blob-service-upload.js';
+import { makeBlobServiceEndpointURL } from '../utils/blob-service.js';
 import type { CallServerEndpoint } from '../utils/call-server-endpoint.js';
-import type { UploadBlob } from '../utils/upload-blob.js';
+import { getMessageForException } from '../utils/errors.js';
+import { handleHTTPResponseError } from '../utils/services-utils.js';
+import { type UploadBlob } from '../utils/upload-blob.js';
 
 export type MultimediaUploadCallbacks = Shape<{
   onProgress: (percent: number) => void,
   abortHandler: (abort: () => void) => void,
   uploadBlob: UploadBlob,
+  blobServiceUploadHandler: BlobServiceUploadHandler,
+  timeout: ?number,
 }>;
 export type MultimediaUploadExtras = Shape<{
   ...Dimensions,
@@ -102,8 +115,119 @@
     await callServerEndpoint('delete_upload', { id });
   };
 
+export type BlobServiceUploadFile =
+  | { +type: 'file', +file: File }
+  | {
+      +type: 'uri',
+      +uri: string,
+      +filename: string,
+      +mimeType: string,
+    };
+
+export type BlobServiceUploadInput = {
+  +blobData: BlobServiceUploadFile,
+  +blobHash: string,
+  +encryptionKey: string,
+  +dimensions: ?Dimensions,
+  +thumbHash?: ?string,
+  +loop?: boolean,
+};
+
+export type BlobServiceUploadAction = (args: {
+  +input: BlobServiceUploadInput,
+  +callbacks?: MultimediaUploadCallbacks,
+}) => Promise<{ ...UploadMultimediaResult, blobHolder: ?string }>;
+
+const blobServiceUpload =
+  (callServerEndpoint: CallServerEndpoint): BlobServiceUploadAction =>
+  async args => {
+    const { input, callbacks } = args;
+    const { encryptionKey, loop, dimensions, thumbHash, blobData } = input;
+    const blobHolder = uuid.v4();
+    const blobHash = toBase64URL(input.blobHash);
+
+    // 1. Assign new holder for blob with given blobHash
+    let blobAlreadyExists: boolean;
+    try {
+      const assignHolderEndpoint = blobService.httpEndpoints.ASSIGN_HOLDER;
+      const assignHolderResponse = await fetch(
+        makeBlobServiceEndpointURL(assignHolderEndpoint),
+        {
+          method: assignHolderEndpoint.method,
+          body: JSON.stringify({
+            holder: blobHolder,
+            blob_hash: blobHash,
+          }),
+          headers: {
+            'content-type': 'application/json',
+          },
+        },
+      );
+      handleHTTPResponseError(assignHolderResponse);
+      const { data_exists: dataExistsResponse } =
+        await assignHolderResponse.json();
+      blobAlreadyExists = dataExistsResponse;
+    } catch (e) {
+      throw new Error(
+        `Failed to assign holder: ${
+          getMessageForException(e) ?? 'unknown error'
+        }`,
+      );
+    }
+
+    // 2. Upload blob contents if blob doesn't exist
+    if (!blobAlreadyExists) {
+      const uploadEndpoint = blobService.httpEndpoints.UPLOAD_BLOB;
+      let blobServiceUploadCallback = blobServiceUploadHandler;
+      if (callbacks && callbacks.blobServiceUploadHandler) {
+        blobServiceUploadCallback = callbacks.blobServiceUploadHandler;
+      }
+      try {
+        await blobServiceUploadCallback(
+          makeBlobServiceEndpointURL(uploadEndpoint),
+          uploadEndpoint.method,
+          {
+            blobHash,
+            blobData,
+          },
+          { ...callbacks },
+        );
+      } catch (e) {
+        throw new Error(
+          `Failed to upload blob: ${
+            getMessageForException(e) ?? 'unknown error'
+          }`,
+        );
+      }
+    }
+
+    // 3. Upload metadata to keyserver
+    const response = await callServerEndpoint('upload_media_metadata', {
+      blobHash,
+      blobHolder,
+      encryptionKey,
+      filename:
+        blobData.type === 'file' ? blobData.file.name : blobData.filename,
+      mimeType:
+        blobData.type === 'file' ? blobData.file.type : blobData.mimeType,
+      loop,
+      thumbHash,
+      ...dimensions,
+    });
+
+    return {
+      id: response.id,
+      uri: response.uri,
+      mediaType: response.mediaType,
+      dimensions: response.dimensions,
+      loop: response.loop,
+      blobHolder,
+    };
+  };
+
 export {
   uploadMultimedia,
+  blobServiceUpload,
   uploadMediaMetadata,
   updateMultimediaMessageMediaActionType,
   deleteUpload,
diff --git a/lib/utils/blob-service-upload.js b/lib/utils/blob-service-upload.js
new file mode 100644
--- /dev/null
+++ b/lib/utils/blob-service-upload.js
@@ -0,0 +1,83 @@
+// @flow
+
+import invariant from 'invariant';
+import _throttle from 'lodash/throttle.js';
+
+import type {
+  MultimediaUploadCallbacks,
+  BlobServiceUploadFile,
+} from '../actions/upload-actions.js';
+
+function blobServiceUploadHandler(
+  url: string,
+  method: string,
+  input: {
+    blobHash: string,
+    blobData: BlobServiceUploadFile,
+  },
+  options?: ?MultimediaUploadCallbacks,
+): Promise<void> {
+  if (input.blobData.type !== 'file') {
+    throw new Error('Use file to upload blob to blob service!');
+  }
+  const formData = new FormData();
+  formData.append('blob_hash', input.blobHash);
+  invariant(input.blobData.file, 'file should be defined');
+  formData.append('blob_data', input.blobData.file);
+
+  const xhr = new XMLHttpRequest();
+  xhr.open(method, url);
+
+  if (options && options.timeout) {
+    xhr.timeout = options.timeout;
+  }
+
+  if (options && options.onProgress) {
+    const { onProgress } = options;
+    xhr.upload.onprogress = _throttle(
+      ({ loaded, total }) => onProgress(loaded / total),
+      50,
+    );
+  }
+
+  let failed = false;
+  const responsePromise = new Promise((resolve, reject) => {
+    xhr.onload = () => {
+      if (failed) {
+        return;
+      }
+      resolve();
+    };
+    xhr.onabort = () => {
+      failed = true;
+      reject(new Error('request aborted'));
+    };
+    xhr.onerror = event => {
+      failed = true;
+      reject(event);
+    };
+    if (options && options.timeout) {
+      xhr.ontimeout = event => {
+        failed = true;
+        reject(event);
+      };
+    }
+    if (options && options.abortHandler) {
+      options.abortHandler(() => {
+        failed = true;
+        reject(new Error('request aborted'));
+        xhr.abort();
+      });
+    }
+  });
+
+  if (!failed) {
+    xhr.send(formData);
+  }
+
+  return responsePromise;
+}
+
+export type BlobServiceUploadHandler = typeof blobServiceUploadHandler;
+
+export { blobServiceUploadHandler };