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
@@ -10,6 +10,7 @@
 import { useKeyserverCall } from '../keyserver-conn/keyserver-call.js';
 import type { CallKeyserverEndpoint } from '../keyserver-conn/keyserver-conn-types.js';
 import { type PerformHTTPMultipartUpload } from '../keyserver-conn/multipart-upload.js';
+import { mediaConfig } from '../media/file-utils.js';
 import { IdentityClientContext } from '../shared/identity-client-context.js';
 import type { AuthMetadata } from '../shared/identity-client-context.js';
 import type { UploadMultimediaResult, Dimensions } from '../types/media-types';
@@ -18,7 +19,10 @@
   blobServiceUploadHandler,
   type BlobServiceUploadHandler,
 } from '../utils/blob-service-upload.js';
-import { makeBlobServiceEndpointURL } from '../utils/blob-service.js';
+import {
+  makeBlobServiceEndpointURL,
+  makeBlobServiceURI,
+} from '../utils/blob-service.js';
 import { getMessageForException } from '../utils/errors.js';
 import {
   handleHTTPResponseError,
@@ -151,7 +155,8 @@
 
 export type BlobServiceUploadAction = (input: {
   +uploadInput: BlobServiceUploadInput,
-  +keyserverOrThreadID: string,
+  // use `null` to skip metadata upload to keyserver
+  +keyserverOrThreadID: ?string,
   +callbacks?: MultimediaUploadCallbacks,
 }) => Promise<BlobServiceUploadResult>;
 
@@ -225,10 +230,37 @@
       }
     }
 
-    // 3. Upload metadata to keyserver
-    const keyserverID: string =
-      extractKeyserverIDFromIDOptional(keyserverOrThreadID) ??
-      keyserverOrThreadID;
+    // 3. Optionally upload metadata to keyserver
+    let maybeKeyserverID;
+    if (keyserverOrThreadID) {
+      maybeKeyserverID =
+        extractKeyserverIDFromIDOptional(keyserverOrThreadID) ??
+        keyserverOrThreadID;
+    }
+    const mimeType =
+      blobInput.type === 'file' ? blobInput.file.type : blobInput.mimeType;
+
+    if (!maybeKeyserverID) {
+      if (!dimensions) {
+        throw new Error('dimensions are required for non-keyserver uploads');
+      }
+
+      const mediaType = mediaConfig[mimeType]?.mediaType;
+      if (mediaType !== 'photo' && mediaType !== 'video') {
+        throw new Error(`mediaType for ${mimeType} should be photo or video`);
+      }
+      return {
+        id: uuid.v4(),
+        uri: makeBlobServiceURI(blobHash),
+        mediaType,
+        dimensions,
+        loop: loop ?? false,
+        blobHolder,
+      };
+    }
+
+    // for Flow
+    const keyserverID: string = maybeKeyserverID;
     const requests = {
       [keyserverID]: {
         blobHash,
@@ -236,8 +268,7 @@
         encryptionKey,
         filename:
           blobInput.type === 'file' ? blobInput.file.name : blobInput.filename,
-        mimeType:
-          blobInput.type === 'file' ? blobInput.file.type : blobInput.mimeType,
+        mimeType,
         loop,
         thumbHash,
         ...dimensions,