diff --git a/keyserver/src/responders/website-responders.js b/keyserver/src/responders/website-responders.js
--- a/keyserver/src/responders/website-responders.js
+++ b/keyserver/src/responders/website-responders.js
@@ -47,7 +47,7 @@
 import { ServerError } from 'lib/utils/errors.js';
 import { promiseAll } from 'lib/utils/promises.js';
 import { defaultNotifPermissionAlertInfo } from 'lib/utils/push-alerts.js';
-import { infoFromURL } from 'lib/utils/url-utils.js';
+import { infoFromURL, urlInfoValidator } from 'lib/utils/url-utils.js';
 import {
   tBool,
   tNumber,
@@ -77,7 +77,7 @@
   getAppURLFactsFromRequestURL,
   getCommAppURLFacts,
 } from '../utils/urls.js';
-import { validateOutput } from '../utils/validation-utils.js';
+import { validateOutput, validateInput } from '../utils/validation-utils.js';
 
 const { renderToNodeStream } = ReactDOMServer;
 
@@ -274,7 +274,16 @@
 
   const initialNavInfoPromise = (async () => {
     try {
-      const urlInfo = infoFromURL(req.url);
+      let urlInfo = infoFromURL(decodeURI(req.url));
+
+      try {
+        urlInfo = await validateInput(viewer, urlInfoValidator, urlInfo, true);
+      } catch (exc) {
+        // We should still be able to handle older links
+        if (exc.message !== 'invalid_client_id_prefix') {
+          throw exc;
+        }
+      }
 
       let backupInfo = {
         now: currentDateInTimeZone(viewer.timeZone),
diff --git a/keyserver/src/utils/validation-utils.js b/keyserver/src/utils/validation-utils.js
--- a/keyserver/src/utils/validation-utils.js
+++ b/keyserver/src/utils/validation-utils.js
@@ -31,8 +31,9 @@
   viewer: Viewer,
   inputValidator: TType<T>,
   input: mixed,
+  ignoreViewerVersion?: boolean,
 ): Promise<T> {
-  if (!viewer.isSocket) {
+  if (!ignoreViewerVersion && !viewer.isSocket) {
     await checkClientSupported(viewer, inputValidator, input);
   }
   const convertedInput = checkInputValidator(inputValidator, input);
@@ -41,7 +42,8 @@
     hasMinStateVersion(viewer.platformDetails, {
       native: 43,
       web: 3,
-    })
+    }) ||
+    ignoreViewerVersion
   ) {
     try {
       return convertClientIDsToServerIDs(
diff --git a/lib/utils/url-utils.js b/lib/utils/url-utils.js
--- a/lib/utils/url-utils.js
+++ b/lib/utils/url-utils.js
@@ -1,6 +1,8 @@
 // @flow
 
-import { idSchemaRegex } from './validation-utils.js';
+import t, { type TInterface } from 'tcomb';
+
+import { idSchemaRegex, tID, tShape } from './validation-utils.js';
 import { pendingThreadIDRegex } from '../shared/thread-utils.js';
 
 export type URLInfo = {
@@ -18,6 +20,20 @@
   ...
 };
 
+export const urlInfoValidator: TInterface<URLInfo> = tShape<URLInfo>({
+  year: t.maybe(t.Number),
+  month: t.maybe(t.Number),
+  verify: t.maybe(t.String),
+  calendar: t.maybe(t.Boolean),
+  chat: t.maybe(t.Boolean),
+  thread: t.maybe(tID),
+  settings: t.maybe(t.enums.of(['account', 'danger-zone'])),
+  threadCreation: t.maybe(t.Boolean),
+  selectedUserList: t.maybe(t.list(t.String)),
+  inviteSecret: t.maybe(t.String),
+  qrCode: t.maybe(t.Boolean),
+});
+
 // We use groups to capture parts of the URL and any changes
 // to regexes must be reflected in infoFromURL.
 const yearRegex = new RegExp('(/|^)year/([0-9]+)(/|$)', 'i');