diff --git a/duniterpy/api/ws2p/network.py b/duniterpy/api/ws2p/network.py
index e39e6912b95a785af6facb5ace86547d5f405f03..6774bcc77ed89508f307a1a619d08ca05d950947 100644
--- a/duniterpy/api/ws2p/network.py
+++ b/duniterpy/api/ws2p/network.py
@@ -69,7 +69,8 @@ WS2P_CONNECT_MESSAGE_SCHEMA = {
     "type": "object",
     "properties": {
         "auth": {
-            "type": "string"
+            "type": "string",
+            "pattern": "^CONNECT$"
         },
         "challenge": {
             "type": "string",
@@ -83,5 +84,37 @@ WS2P_CONNECT_MESSAGE_SCHEMA = {
         "sig": {
             "type": "string",
         },
-    }
+    },
+    "required": ["auth", "challenge", "currency", "pub", "sig"]
+}
+
+WS2P_ACK_MESSAGE_SCHEMA = {
+    "type": "object",
+    "properties": {
+        "auth": {
+            "type": "string",
+            "pattern": "^ACK$"
+        },
+        "pub": {
+            "type": "string",
+        },
+        "sig": {
+            "type": "string",
+        }
+    },
+    "required": ["auth", "pub", "sig"]
+}
+
+WS2P_OK_MESSAGE_SCHEMA = {
+    "type": "object",
+    "properties": {
+        "auth": {
+            "type": "string",
+            "pattern": "^OK$"
+        },
+        "sig": {
+            "type": "string",
+        }
+    },
+    "required": ["auth", "sig"]
 }
diff --git a/duniterpy/documents/ws2p/messages.py b/duniterpy/documents/ws2p/messages.py
new file mode 100644
index 0000000000000000000000000000000000000000..62d3d8d88f2e36376e46f3a42d73a2110be24ae8
--- /dev/null
+++ b/duniterpy/documents/ws2p/messages.py
@@ -0,0 +1,169 @@
+import json
+import uuid
+from typing import Optional
+
+from duniterpy.documents import Document
+from duniterpy.key import VerifyingKey, SigningKey
+
+
+class Connect(Document):
+    version = 2
+    auth = "CONNECT"
+
+    def __init__(self, currency: str, pubkey: str, challenge: Optional[str] = None,
+                 signature: Optional[str] = None) -> None:
+        """
+        Init Connect message document
+
+        :param currency: Name of currency
+        :param pubkey: Public key of node
+        :param challenge: [Optional, default=None] Big random string, typically an uuid
+        :param signature: [Optional, default=None] Base64 encoded signature of raw formated document
+        """
+        super().__init__(self.version, currency, [signature])
+
+        self.pubkey = pubkey
+        if challenge is None:
+            # create challenge
+            self.challenge = uuid.uuid4().hex + uuid.uuid4().hex
+        else:
+            self.challenge = challenge
+        # add and verify signature
+        if signature is not None:
+            self.signatures.append(signature)
+            verifying_key = VerifyingKey(self.pubkey)
+            verifying_key.verify_document(self)
+
+    def raw(self):
+        """
+        Return the document in raw format
+
+        :return:
+        """
+        return "WS2P:CONNECT:{currency}:{pub}:{challenge}".format(currency=self.currency, pub=self.pubkey,
+                                                                  challenge=self.challenge)
+
+    def get_signed_json(self, signing_key: SigningKey) -> str:
+        """
+        Return the signed document in json format
+
+        :param signing_key: Signing key instance
+
+        :return:
+        """
+        self.sign([signing_key])
+        data = {
+            "auth": self.auth,
+            "pub": self.pubkey,
+            "challenge": self.challenge,
+            "sig": self.signatures[0]
+        }
+        return json.dumps(data)
+
+    def __str__(self) -> str:
+        return self.raw()
+
+
+class Ack(Document):
+    version = 2
+    auth = "ACK"
+
+    def __init__(self, currency: str, pubkey: str, challenge: str,
+                 signature: Optional[str] = None) -> None:
+        """
+        Init Connect message document
+
+        :param currency: Name of currency
+        :param pubkey: Public key of node
+        :param challenge: The challenge sent in the connect message
+        :param signature: [Optional, default=None] Base64 encoded signature of raw formated document
+        """
+        super().__init__(self.version, currency, [signature])
+
+        self.pubkey = pubkey
+        self.challenge = challenge
+        # add and verify signature
+        if signature is not None:
+            self.signatures.append(signature)
+            verifying_key = VerifyingKey(self.pubkey)
+            verifying_key.verify_document(self)
+
+    def raw(self):
+        """
+        Return the document in raw format
+
+        :return:
+        """
+        return "WS2P:ACK:{currency}:{pub}:{challenge}".format(currency=self.currency, pub=self.pubkey,
+                                                              challenge=self.challenge)
+
+    def get_signed_json(self, signing_key: SigningKey) -> str:
+        """
+        Return the signed document in json format
+
+        :param signing_key: Signing key instance
+
+        :return:
+        """
+        self.sign([signing_key])
+        data = {
+            "auth": self.auth,
+            "pub": self.pubkey,
+            "sig": self.signatures[0]
+        }
+        return json.dumps(data)
+
+    def __str__(self) -> str:
+        return self.raw()
+
+
+class Ok(Document):
+    version = 2
+    auth = "OK"
+
+    def __init__(self, currency: str, pubkey: str, challenge: str,
+                 signature: Optional[str] = None) -> None:
+        """
+        Init Connect message document
+
+        :param currency: Name of currency
+        :param pubkey: Public key of node
+        :param challenge: The challenge sent in the connect message
+        :param signature: [Optional, default=None] Base64 encoded signature of raw formated document
+        """
+        super().__init__(self.version, currency, [signature])
+
+        self.pubkey = pubkey
+        self.challenge = challenge
+        # add and verify signature
+        if signature is not None:
+            self.signatures.append(signature)
+            verifying_key = VerifyingKey(self.pubkey)
+            verifying_key.verify_document(self)
+
+    def raw(self):
+        """
+        Return the document in raw format
+
+        :return:
+        """
+        return "WS2P:OK:{currency}:{pub}:{challenge}".format(currency=self.currency, pub=self.pubkey,
+                                                             challenge=self.challenge)
+
+    def get_signed_json(self, signing_key: SigningKey) -> str:
+        """
+        Return the signed document in json format
+
+        :param signing_key: Signing key instance
+
+        :return:
+        """
+        self.sign([signing_key])
+        data = {
+            "auth": self.auth,
+            "sig": self.signatures[0]
+        }
+        return json.dumps(data)
+
+    def __str__(self) -> str:
+        return self.raw()
diff --git a/examples/request_web_socket_ws2p.py b/examples/request_web_socket_ws2p.py
deleted file mode 100644
index f4f5424586615384ff1e3238147da4dce3b1ef7d..0000000000000000000000000000000000000000
--- a/examples/request_web_socket_ws2p.py
+++ /dev/null
@@ -1,76 +0,0 @@
-import asyncio
-from _socket import gaierror
-
-import aiohttp
-import jsonschema
-
-from duniterpy.api import ws2p
-from duniterpy.api.client import Client, parse_text
-
-# CONFIG #######################################
-
-# You can either use a complete defined endpoint : [NAME_OF_THE_API] [DOMAIN] [IPv4] [IPv6] [PORT]
-# or the simple definition : [NAME_OF_THE_API] [DOMAIN] [PORT]
-# Here we use the WS2P API (WS2P)
-WS2P_ENDPOINT = "WS2P 2f731dcd 127.0.0.1 20900"
-
-
-################################################
-
-
-async def main():
-    """
-    Main code
-    """
-    # Create Client from endpoint string in Duniter format
-    client = Client(WS2P_ENDPOINT)
-
-    try:
-        # Create Web Socket connection on block path
-        ws_connection = client.connect_ws()
-
-        # From the documentation ws_connection should be a ClientWebSocketResponse object...
-        #
-        # https://docs.aiohttp.org/en/stable/client_quickstart.html#websockets
-        #
-        # In reality, aiohttp.session.ws_connect() returns a aiohttp.client._WSRequestContextManager instance.
-        # It must be used in a with statement to get the ClientWebSocketResponse instance from it (__aenter__).
-        # At the end of the with statement, aiohttp.client._WSRequestContextManager.__aexit__ is called
-        # and close the ClientWebSocketResponse in it.
-        connect_message = """
-        """
-        #await ws_connection.send(connect_message)
-
-        # Mandatory to get the "for msg in ws" to work !
-        async with ws_connection as ws:
-            print("Connected successfully to web socket block path")
-
-            # Iterate on each message received...
-            async for msg in ws:
-                # if message type is text...
-                if msg.type == aiohttp.WSMsgType.TEXT:
-                    print("Received a message")
-                    # Validate jsonschema and return a the json dict
-                    data = parse_text(msg.data, ws2p.network.WS2P_CONNECT_MESSAGE_SCHEMA)
-                    print(data)
-                elif msg.type == aiohttp.WSMsgType.CLOSED:
-                    # Connection is closed
-                    print("Web socket connection closed !")
-                elif msg.type == aiohttp.WSMsgType.ERROR:
-                    # Connection error
-                    print("Web socket connection error !")
-
-                # Close session
-                await client.close()
-
-    except (aiohttp.WSServerHandshakeError, ValueError) as e:
-        print("Websocket block {0} : {1}".format(type(e).__name__, str(e)))
-    except (aiohttp.ClientError, gaierror, TimeoutError) as e:
-        print("{0} : {1}".format(str(e), WS2P_ENDPOINT))
-    except jsonschema.ValidationError as e:
-        print("{:}:{:}".format(str(e.__class__.__name__), str(e)))
-
-
-# Latest duniter-python-api is asynchronous and you have to use asyncio, an asyncio loop and a "as" on the data.
-# ( https://docs.python.org/3/library/asyncio.html )
-asyncio.get_event_loop().run_until_complete(main())
diff --git a/examples/request_ws2p.py b/examples/request_ws2p.py
new file mode 100644
index 0000000000000000000000000000000000000000..7d4375d8888d223facb31e11e1d16f98034cf2bd
--- /dev/null
+++ b/examples/request_ws2p.py
@@ -0,0 +1,134 @@
+import asyncio
+
+from _socket import gaierror
+
+import aiohttp
+import jsonschema
+
+from duniterpy.api import ws2p
+from duniterpy.documents.ws2p.messages import Connect, Ack, Ok
+from duniterpy.api.client import Client, parse_text
+
+# CONFIG #######################################
+
+# You can either use a complete defined endpoint : [NAME_OF_THE_API] [DOMAIN] [IPv4] [IPv6] [PORT]
+# or the simple definition : [NAME_OF_THE_API] [DOMAIN] [PORT]
+# Here we use the WS2P API (WS2P)
+from duniterpy.key import SigningKey
+
+WS2P_ENDPOINT = "WS2P 2f731dcd 127.0.0.1 20900"
+CURRENCY = "g1-test"
+
+
+################################################
+
+
+async def main():
+    """
+    Main code
+    """
+
+    # # prompt hidden user entry
+    # salt = getpass.getpass("Enter your passphrase (salt): ")
+    #
+    # # prompt hidden user entry
+    # password = getpass.getpass("Enter your password: ")
+    salt = password = "toto"
+
+    # init signing_key instance
+    signing_key = SigningKey.from_credentials(salt, password)
+
+    connect_document = Connect(CURRENCY, signing_key.pubkey)
+    connect_message = connect_document.get_signed_json(signing_key)
+
+    # Create Client from endpoint string in Duniter format
+    client = Client(WS2P_ENDPOINT)
+
+    try:
+        # Create Web Socket connection on block path
+        ws_connection = client.connect_ws()
+
+        # From the documentation ws_connection should be a ClientWebSocketResponse object...
+        #
+        # https://docs.aiohttp.org/en/stable/client_quickstart.html#websockets
+        #
+        # In reality, aiohttp.session.ws_connect() returns a aiohttp.client._WSRequestContextManager instance.
+        # It must be used in a with statement to get the ClientWebSocketResponse instance from it (__aenter__).
+        # At the end of the with statement, aiohttp.client._WSRequestContextManager.__aexit__ is called
+        # and close the ClientWebSocketResponse in it.
+
+        # Mandatory to get the "for msg in ws" to work !
+        async with ws_connection as ws:
+            print("Connected successfully to web socket endpoint")
+
+            print("Send message CONNECT : " + connect_message)
+            await ws.send_str(connect_message)
+
+            # Iterate on each message received...
+            async for msg in ws:
+                # if message type is text...
+                if msg.type == aiohttp.WSMsgType.TEXT:
+                    print(msg.data)
+                    try:
+                        # Validate jsonschema and return a the json dict
+                        data = parse_text(msg.data, ws2p.network.WS2P_CONNECT_MESSAGE_SCHEMA)
+                        print("Received a CONNECT message")
+                        remote_connect_document = Connect(CURRENCY, data["pub"], data["challenge"], data["sig"])
+                        print("Remote CONNECT message signature is valid")
+
+                        ack_message = Ack(CURRENCY, signing_key.pubkey,
+                                          remote_connect_document.challenge).get_signed_json(
+                            signing_key)
+                        # send ACK message
+                        print("Send ACK message...")
+                        await ws.send_str(ack_message)
+
+                    except jsonschema.exceptions.ValidationError:
+                        try:
+                            # Validate jsonschema and return a the json dict
+                            data = parse_text(msg.data, ws2p.network.WS2P_ACK_MESSAGE_SCHEMA)
+                            print("Received a ACK message")
+
+                            # create ACK document from ACK response to verify signature
+                            Ack(CURRENCY, data["pub"], connect_document.challenge, data["sig"])
+                            print("Remote ACK message signature is valid")
+                            # Si ACK response ok, create OK message
+                            ok_message = Ok(CURRENCY, signing_key.pubkey, connect_document.challenge).get_signed_json(
+                                signing_key)
+
+                            # send OK message
+                            print("Send OK message...")
+                            await ws.send_str(ok_message)
+                        except jsonschema.exceptions.ValidationError:
+                            try:
+                                # Validate jsonschema and return a the json dict
+                                data = parse_text(msg.data, ws2p.network.WS2P_OK_MESSAGE_SCHEMA)
+                                print("Received a OK message")
+
+                                Ok(CURRENCY, remote_connect_document.pubkey, connect_document.challenge, data["sig"])
+                                print("Remote OK message signature is valid")
+
+                            except jsonschema.exceptions.ValidationError:
+                                pass
+
+                elif msg.type == aiohttp.WSMsgType.CLOSED:
+                    # Connection is closed
+                    print("Web socket connection closed !")
+                elif msg.type == aiohttp.WSMsgType.ERROR:
+                    # Connection error
+                    print("Web socket connection error !")
+
+            # Close session
+            await client.close()
+
+    except (aiohttp.WSServerHandshakeError, ValueError) as e:
+        print("Websocket handshake {0} : {1}".format(type(e).__name__, str(e)))
+    except (aiohttp.ClientError, gaierror, TimeoutError) as e:
+        print("{0} : {1}".format(str(e), WS2P_ENDPOINT))
+    except jsonschema.ValidationError as e:
+        print("{:}:{:}".format(str(e.__class__.__name__), str(e)))
+
+
+# Latest duniter-python-api is asynchronous and you have to use asyncio, an asyncio loop and a "as" on the data.
+# ( https://docs.python.org/3/library/asyncio.html )
+asyncio.get_event_loop().run_until_complete(main())