diff --git a/declearn/communication/api/backend/_handler.py b/declearn/communication/api/backend/_handler.py
index 88c5c95d73d809aa8a0457f79833eac8821643d0..f200850baf7a16cd4c4de5ae5c602ecc370e9baa 100644
--- a/declearn/communication/api/backend/_handler.py
+++ b/declearn/communication/api/backend/_handler.py
@@ -29,6 +29,8 @@ from declearn.communication.api.backend.actions import (
     ActionMessage,
     Drop,
     Join,
+    LegacyMessageError,
+    LegacyReject,
     Ping,
     Recv,
     Reject,
@@ -103,9 +105,13 @@ class MessagesHandler:
             message = parse_action_from_string(string)
         except (KeyError, TypeError, ValueError) as exc:
             self.logger.info(
-                "%s encountered while parsing received message: %s", repr(exc)
+                "Exception encountered while parsing received message: %s",
+                repr(exc),
             )
             return Reject(flags.INVALID_MESSAGE)
+        except LegacyMessageError as exc:
+            self.logger.info(repr(exc))
+            return LegacyReject()
         # Case: join request from a (new) client. Handle it.
         if isinstance(message, Join):
             return await self._handle_join_request(message, context)
@@ -169,9 +175,7 @@ class MessagesHandler:
         message: Join,
     ) -> Optional[Reject]:
         """Return an 'Error' if a 'JoinRequest' is of incompatible version."""
-        if message.version is None:
-            message.version = "<2.4"
-        elif message.version.split(".")[:2] == VERSION.split(".")[:2]:
+        if message.version.split(".")[:2] == VERSION.split(".")[:2]:
             return None
         self.logger.info(
             "Received a registration request under name %s, that is "
diff --git a/declearn/communication/api/backend/actions.py b/declearn/communication/api/backend/actions.py
index e5c28d7d8fe9d4fd218948a01ed855dcc977eca9..974e2f062c930503ea40f03760b2fe13ec4c2a3e 100644
--- a/declearn/communication/api/backend/actions.py
+++ b/declearn/communication/api/backend/actions.py
@@ -37,11 +37,16 @@ import dataclasses
 import json
 from typing import Optional
 
+
+from declearn.version import VERSION
+
 __all__ = [
     "Accept",
     "ActionMessage",
     "Drop",
     "Join",
+    "LegacyReject",
+    "LegacyMessageError",
     "Ping",
     "Recv",
     "Reject",
@@ -50,6 +55,10 @@ __all__ = [
 ]
 
 
+class LegacyMessageError(Exception):
+    """Custom exception to denote legacy Message being received."""
+
+
 @dataclasses.dataclass
 class ActionMessage(metaclass=abc.ABCMeta):
     """Abstract base class for fundamental messages."""
@@ -82,7 +91,7 @@ class Join(ActionMessage):
     """Client action message to request joining a server."""
 
     name: str
-    version: Optional[str] = None
+    version: str
 
 
 @dataclasses.dataclass
@@ -153,6 +162,10 @@ def parse_action_from_string(
     except json.JSONDecodeError as exc:
         raise ValueError("Failed to parse 'ActionMessage' string.") from exc
     if "action" not in data:
+        if "typekey" in data:
+            raise LegacyMessageError(
+                f"Received a legacy message with type '{data['typekey']}'."
+            )
         raise ValueError(
             "Failed to parse 'ActionMessage' string: no 'action' key."
         )
@@ -164,3 +177,21 @@ def parse_action_from_string(
             f"'{data['action']}' key."
         )
     return cls(**data)
+
+
+@dataclasses.dataclass
+class LegacyReject(ActionMessage):
+    """Server action to reject a legacy client's (registration) message.
+
+    This message will be serialized in a way that is compatible with the
+    legacy message parser, but not with the current one.
+    """
+
+    def to_string(
+        self,
+    ) -> str:
+        message = (
+            "Cannot communicate due to the DecLearn version in use. "
+            f"Please update to `declearn ~= {VERSION}`."
+        )
+        return json.dumps({"typekey": "error", "message": message})