diff --git a/keyserver/src/updaters/thread-updaters.js b/keyserver/src/updaters/thread-updaters.js
--- a/keyserver/src/updaters/thread-updaters.js
+++ b/keyserver/src/updaters/thread-updaters.js
@@ -2,10 +2,7 @@
 
 import { getRolePermissionBlobs } from 'lib/permissions/thread-permissions.js';
 import { filteredThreadIDs } from 'lib/selectors/calendar-filter-selectors.js';
-import {
-  getPinnedContentFromMessage,
-  isInvalidPinSource,
-} from 'lib/shared/message-utils.js';
+import { getPinnedContentFromMessage } from 'lib/shared/message-utils.js';
 import {
   threadHasAdminRole,
   roleIsAdminRole,
@@ -32,6 +29,7 @@
 import { ServerError } from 'lib/utils/errors.js';
 import { promiseAll } from 'lib/utils/promises.js';
 import { firstLine } from 'lib/utils/string-utils.js';
+import { canToggleMessagePin } from 'lib/utils/toggle-pin-utils.js';
 import { validChatNameRegex } from 'lib/utils/validation-utils.js';
 
 import { reportLinkUsage } from './link-updaters.js';
@@ -50,6 +48,7 @@
   fetchThreadInfos,
   fetchServerThreadInfos,
   determineThreadAncestry,
+  rawThreadInfosFromServerThreadInfos,
 } from '../fetchers/thread-fetchers.js';
 import {
   checkThreadPermission,
@@ -876,18 +875,23 @@
   const { messageID, action } = request;
 
   const targetMessage = await fetchMessageInfoByID(viewer, messageID);
-  if (!targetMessage || isInvalidPinSource(targetMessage)) {
+  if (!targetMessage) {
     throw new ServerError('invalid_parameters');
   }
 
   const { threadID } = targetMessage;
-  const hasPermission = await checkThreadPermission(
-    viewer,
+  const fetchServerThreadInfosResult = await fetchServerThreadInfos({
     threadID,
-    threadPermissions.MANAGE_PINS,
+  });
+  const { threadInfos: rawThreadInfos } = rawThreadInfosFromServerThreadInfos(
+    viewer,
+    fetchServerThreadInfosResult,
   );
-  if (!hasPermission) {
-    throw new ServerError('invalid_credentials');
+  const rawThreadInfo = rawThreadInfos[threadID];
+
+  const canTogglePin = canToggleMessagePin(targetMessage, rawThreadInfo);
+  if (!canTogglePin) {
+    throw new ServerError('invalid_parameters');
   }
 
   const pinnedValue = action === 'pin' ? 1 : 0;
@@ -929,9 +933,7 @@
   };
 
   const createUpdatesAsync = async () => {
-    const { threadInfos: serverThreadInfos } = await fetchServerThreadInfos({
-      threadID,
-    });
+    const { threadInfos: serverThreadInfos } = fetchServerThreadInfosResult;
     const time = Date.now();
     const updates = [];
     for (const member of serverThreadInfos[threadID].members) {
diff --git a/lib/shared/message-utils.js b/lib/shared/message-utils.js
--- a/lib/shared/message-utils.js
+++ b/lib/shared/message-utils.js
@@ -41,7 +41,7 @@
   RawReactionMessageInfo,
   ReactionMessageInfo,
 } from '../types/messages/reaction.js';
-import type { ThreadInfo } from '../types/thread-types.js';
+import type { RawThreadInfo, ThreadInfo } from '../types/thread-types.js';
 import type { UserInfos } from '../types/user-types.js';
 import {
   type EntityText,
@@ -678,7 +678,7 @@
 
 function isInvalidPinSourceForThread(
   messageInfo: RawMessageInfo | MessageInfo,
-  threadInfo: ThreadInfo,
+  threadInfo: RawThreadInfo | ThreadInfo,
 ): boolean {
   const isValidPinSource = !isInvalidPinSource(messageInfo);
   const isFirstMessageInSidebar = threadInfo.sourceMessageID === messageInfo.id;
diff --git a/lib/utils/toggle-pin-utils.js b/lib/utils/toggle-pin-utils.js
--- a/lib/utils/toggle-pin-utils.js
+++ b/lib/utils/toggle-pin-utils.js
@@ -2,16 +2,13 @@
 
 import { isInvalidPinSourceForThread } from '../shared/message-utils.js';
 import { threadHasPermission } from '../shared/thread-utils.js';
-import type {
-  ComposableMessageInfo,
-  RobotextMessageInfo,
-} from '../types/message-types.js';
+import type { RawMessageInfo, MessageInfo } from '../types/message-types.js';
 import { threadPermissions } from '../types/thread-permission-types.js';
-import type { ThreadInfo } from '../types/thread-types.js';
+import type { RawThreadInfo, ThreadInfo } from '../types/thread-types.js';
 
 function canToggleMessagePin(
-  messageInfo: ComposableMessageInfo | RobotextMessageInfo,
-  threadInfo: ThreadInfo,
+  messageInfo: RawMessageInfo | MessageInfo,
+  threadInfo: RawThreadInfo | ThreadInfo,
 ): boolean {
   const isValidMessage = !isInvalidPinSourceForThread(messageInfo, threadInfo);
   const hasManagePinsPermission = threadHasPermission(