diff --git a/keyserver/src/endpoints.js b/keyserver/src/endpoints.js
--- a/keyserver/src/endpoints.js
+++ b/keyserver/src/endpoints.js
@@ -44,6 +44,7 @@
   threadCreationResponder,
   threadFetchMediaResponder,
   threadJoinResponder,
+  toggleMessagePinResponder,
 } from './responders/thread-responders.js';
 import {
   userSubscriptionUpdateResponder,
@@ -192,6 +193,10 @@
     responder: threadSetUnreadStatusResponder,
     requiredPolicies: baseLegalPolicies,
   },
+  toggle_message_pin: {
+    responder: toggleMessagePinResponder,
+    requiredPolicies: baseLegalPolicies,
+  },
   update_account: {
     responder: passwordUpdateResponder,
     requiredPolicies: baseLegalPolicies,
diff --git a/keyserver/src/responders/thread-responders.js b/keyserver/src/responders/thread-responders.js
--- a/keyserver/src/responders/thread-responders.js
+++ b/keyserver/src/responders/thread-responders.js
@@ -17,6 +17,7 @@
   type ThreadJoinResult,
   type ThreadFetchMediaResult,
   type ThreadFetchMediaRequest,
+  type ToggleMessagePinRequest,
   threadTypes,
 } from 'lib/types/thread-types.js';
 import { updateUserAvatarRequestValidator } from 'lib/utils/avatar-utils.js';
@@ -42,6 +43,7 @@
   leaveThread,
   updateThread,
   joinThread,
+  toggleMessagePinForThread,
 } from '../updaters/thread-updaters.js';
 import { validateInput } from '../utils/validation-utils.js';
 
@@ -195,6 +197,19 @@
   return await fetchMediaForThread(viewer, request);
 }
 
+const toggleMessagePinRequestInputValidator = tShape({
+  messageID: t.String,
+  action: t.enums.of(['pin', 'unpin']),
+});
+async function toggleMessagePinResponder(
+  viewer: Viewer,
+  input: any,
+): Promise<void> {
+  const request: ToggleMessagePinRequest = input;
+  await validateInput(viewer, toggleMessagePinRequestInputValidator, request);
+  await toggleMessagePinForThread(viewer, request);
+}
+
 export {
   threadDeletionResponder,
   roleUpdateResponder,
@@ -205,4 +220,5 @@
   threadJoinResponder,
   threadFetchMediaResponder,
   newThreadRequestInputValidator,
+  toggleMessagePinResponder,
 };
diff --git a/lib/actions/thread-actions.js b/lib/actions/thread-actions.js
--- a/lib/actions/thread-actions.js
+++ b/lib/actions/thread-actions.js
@@ -12,6 +12,7 @@
   ThreadJoinPayload,
   ThreadFetchMediaRequest,
   ThreadFetchMediaResult,
+  ToggleMessagePinRequest,
 } from '../types/thread-types.js';
 import type { CallServerEndpoint } from '../utils/call-server-endpoint.js';
 import { values } from '../utils/objects.js';
@@ -174,6 +175,14 @@
     return response;
   };
 
+const toggleMessagePin =
+  (
+    callServerEndpoint: CallServerEndpoint,
+  ): ((request: ToggleMessagePinRequest) => Promise<void>) =>
+  async request => {
+    await callServerEndpoint('toggle_message_pin', request);
+  };
+
 export {
   deleteThreadActionTypes,
   deleteThread,
@@ -190,4 +199,5 @@
   leaveThreadActionTypes,
   leaveThread,
   fetchThreadMedia,
+  toggleMessagePin,
 };
diff --git a/lib/types/endpoints.js b/lib/types/endpoints.js
--- a/lib/types/endpoints.js
+++ b/lib/types/endpoints.js
@@ -73,6 +73,7 @@
   SEND_PASSWORD_RESET_EMAIL: 'send_password_reset_email',
   SEND_VERIFICATION_EMAIL: 'send_verification_email',
   SET_THREAD_UNREAD_STATUS: 'set_thread_unread_status',
+  TOGGLE_MESSAGE_PIN: 'toggle_message_pin',
   UPDATE_ACCOUNT: 'update_account',
   UPDATE_USER_SETTINGS: 'update_user_settings',
   UPDATE_DEVICE_TOKEN: 'update_device_token',