diff --git a/keyserver/src/creators/upload-creator.js b/keyserver/src/creators/upload-creator.js
--- a/keyserver/src/creators/upload-creator.js
+++ b/keyserver/src/creators/upload-creator.js
@@ -32,6 +32,7 @@
   +dimensions: Dimensions,
   +loop: boolean,
   +encryptionKey?: string,
+  +thumbHash?: string,
 };
 async function createUploads(
   viewer: Viewer,
@@ -45,7 +46,8 @@
   const uploadRows = uploadInfos.map(uploadInfo => {
     const id = ids.shift();
     const secret = crypto.randomBytes(8).toString('hex');
-    const { content, dimensions, mediaType, loop, encryptionKey } = uploadInfo;
+    const { content, dimensions, mediaType, loop, encryptionKey, thumbHash } =
+      uploadInfo;
     const buffer =
       content.storage === 'keyserver' ? content.buffer : Buffer.alloc(0);
     const blobHolder =
@@ -69,7 +71,13 @@
         buffer,
         secret,
         Date.now(),
-        JSON.stringify({ ...dimensions, loop, blobHolder, encryptionKey }),
+        JSON.stringify({
+          ...dimensions,
+          loop,
+          blobHolder,
+          encryptionKey,
+          thumbHash,
+        }),
       ],
     };
   });
diff --git a/keyserver/src/uploads/media-utils.js b/keyserver/src/uploads/media-utils.js
--- a/keyserver/src/uploads/media-utils.js
+++ b/keyserver/src/uploads/media-utils.js
@@ -49,6 +49,7 @@
   +inputLoop: boolean,
   +inputEncryptionKey: ?string,
   +inputMimeType: ?string,
+  +inputThumbHash: ?string,
   +size: number, // in bytes
 };
 async function validateAndConvert(
@@ -61,9 +62,15 @@
     inputLoop,
     inputEncryptionKey,
     inputMimeType,
+    inputThumbHash,
     size, // in bytes
   } = input;
 
+  const passthroughParams = {
+    loop: inputLoop,
+    ...(inputThumbHash ? { thumbHash: inputThumbHash } : undefined),
+  };
+
   // we don't want to transcode encrypted files
   if (inputEncryptionKey) {
     invariant(
@@ -81,12 +88,12 @@
     }
 
     return {
+      ...passthroughParams,
       name: initialName,
       mime: inputMimeType,
       mediaType,
       content: { storage: 'keyserver', buffer: initialBuffer },
       dimensions: inputDimensions,
-      loop: inputLoop,
       encryptionKey: inputEncryptionKey,
     };
   }
@@ -106,12 +113,12 @@
       'inputDimensions should be set in validateAndConvert',
     );
     return {
+      ...passthroughParams,
       mime: mime,
       mediaType: mediaType,
       name: initialName,
       content: { storage: 'keyserver', buffer: initialBuffer },
       dimensions: inputDimensions,
-      loop: inputLoop,
     };
   }
 
@@ -120,7 +127,7 @@
     return null;
   }
 
-  return convertImage(
+  const convertedImage = await convertImage(
     initialBuffer,
     mime,
     initialName,
@@ -128,6 +135,14 @@
     inputLoop,
     size,
   );
+  if (!convertedImage) {
+    return null;
+  }
+
+  return {
+    ...passthroughParams,
+    ...convertedImage,
+  };
 }
 
 async function convertImage(
diff --git a/keyserver/src/uploads/uploads.js b/keyserver/src/uploads/uploads.js
--- a/keyserver/src/uploads/uploads.js
+++ b/keyserver/src/uploads/uploads.js
@@ -72,6 +72,11 @@
   if (inputMimeType && typeof inputMimeType !== 'string') {
     throw new ServerError('invalid_parameters');
   }
+  const inputThumbHash =
+    files.length === 1 && body.thumbHash ? body.thumbHash : null;
+  if (inputThumbHash && typeof inputThumbHash !== 'string') {
+    throw new ServerError('invalid_parameters');
+  }
 
   const validationResults = await Promise.all(
     files.map(({ buffer, size, originalname }) =>
@@ -82,6 +87,7 @@
         inputLoop,
         inputEncryptionKey,
         inputMimeType,
+        inputThumbHash,
         size,
       }),
     ),
@@ -102,6 +108,7 @@
   encryptionKey: t.String,
   mimeType: t.String,
   loop: t.maybe(t.Boolean),
+  thumbHash: t.maybe(t.String),
 });
 
 async function uploadMediaMetadataResponder(
@@ -126,6 +133,7 @@
     encryptionKey,
     dimensions: { width, height },
     loop: loop ?? false,
+    thumbHash: request.thumbHash,
   };
 
   const [result] = await createUploads(viewer, [uploadInfo]);
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
@@ -17,7 +17,8 @@
 export type MultimediaUploadExtras = Shape<{
   ...Dimensions,
   loop: boolean,
-  encryptionKey?: string,
+  encryptionKey: string,
+  thumbHash: ?string,
 }>;
 
 const uploadMediaMetadata =
@@ -61,6 +62,9 @@
     if (extras.encryptionKey) {
       stringExtras.encryptionKey = extras.encryptionKey;
     }
+    if (extras.thumbHash) {
+      stringExtras.thumbHash = extras.thumbHash;
+    }
 
     // also pass MIME type if available
     if (multimedia.type && typeof multimedia.type === 'string') {
diff --git a/lib/types/media-types.js b/lib/types/media-types.js
--- a/lib/types/media-types.js
+++ b/lib/types/media-types.js
@@ -82,6 +82,7 @@
   +encryptionKey: string,
   +mimeType: string,
   +loop?: boolean,
+  +thumbHash?: string,
 };
 
 export type FFmpegStatistics = {