diff --git a/src/account.h b/src/account.h
index 4c746aca97c79d086309857a79167525d088ba63..95058f21169fc40f94327d7444c3f3aba3294fee 100644
--- a/src/account.h
+++ b/src/account.h
@@ -55,6 +55,10 @@ class Emitter;
 class Node;
 } // namespace YAML
 
+namespace Json {
+class Value;
+}
+
 namespace jami {
 static constexpr uint64_t DRING_ID_MAX_VAL = 9007199254740992;
 
@@ -319,6 +323,9 @@ public:
                                 const std::string& /*conversationId*/,
                                 const std::string& /*commitId*/) {};
 
+    // Invites
+    virtual void onConversationRequest(const std::string& from, const Json::Value&) {};
+
     /**
      * Helper function used to load the default codec order from the codec factory
      */
diff --git a/src/jamidht/conversation.cpp b/src/jamidht/conversation.cpp
index 00860304959e14148df70165216a2ba29c88c82c..426af1b8df8d34b52982e963c1f1b6999ba90170 100644
--- a/src/jamidht/conversation.cpp
+++ b/src/jamidht/conversation.cpp
@@ -56,6 +56,8 @@ public:
     }
     ~Impl() = default;
 
+    std::string repoPath() const;
+
     std::unique_ptr<ConversationRepository> repository_;
     std::weak_ptr<JamiAccount> account_;
     std::vector<std::map<std::string, std::string>> loadMessages(const std::string& fromMessage = "",
@@ -63,6 +65,16 @@ public:
                                                                  size_t n = 0);
 };
 
+std::string
+Conversation::Impl::repoPath() const
+{
+    auto shared = account_.lock();
+    if (!shared)
+        return {};
+    return fileutils::get_data_dir() + DIR_SEPARATOR_STR + shared->getAccountID()
+           + DIR_SEPARATOR_STR + "conversations" + DIR_SEPARATOR_STR + repository_->id();
+}
+
 std::vector<std::map<std::string, std::string>>
 Conversation::Impl::loadMessages(const std::string& fromMessage,
                                  const std::string& toMessage,
@@ -156,7 +168,7 @@ Conversation::removeMember(const std::string& contactUri)
 }
 
 std::vector<std::map<std::string, std::string>>
-Conversation::getMembers()
+Conversation::getMembers(bool includeInvited) const
 {
     std::vector<std::map<std::string, std::string>> result;
     auto shared = pimpl_->account_.lock();
@@ -168,6 +180,7 @@ Conversation::getMembers()
                     + pimpl_->repository_->id();
     auto adminsPath = repoPath + DIR_SEPARATOR_STR + "admins";
     auto membersPath = repoPath + DIR_SEPARATOR_STR + "members";
+    auto invitedPath = pimpl_->repoPath() + DIR_SEPARATOR_STR + "invited";
     for (const auto& certificate : fileutils::readDirectory(adminsPath)) {
         if (certificate.find(".crt") == std::string::npos) {
             JAMI_WARN("Incorrect file found: %s/%s", adminsPath.c_str(), certificate.c_str());
@@ -188,6 +201,14 @@ Conversation::getMembers()
                      {"role", "member"}};
         result.emplace_back(details);
     }
+    if (includeInvited) {
+        for (const auto& uri : fileutils::readDirectory(invitedPath)) {
+            std::map<std::string, std::string>
+                details {{"uri", uri },
+                        {"role", "invited"}};
+            result.emplace_back(details);
+        }
+    }
 
     return result;
 }
@@ -289,4 +310,28 @@ Conversation::mergeHistory(const std::string& uri)
     return true;
 }
 
+std::map<std::string, std::string>
+Conversation::generateInvitation() const
+{
+    // Invite the new member to the conversation
+    std::map<std::string, std::string> invite;
+    Json::Value root;
+    root["conversationId"] = id();
+    // TODO remove, cause the peer cannot trust?
+    // Or add signatures?
+    for (const auto& member : getMembers()) {
+        Json::Value jsonMember;
+        for (const auto& [key, value] : member) {
+            jsonMember[key] = value;
+        }
+        root["members"].append(jsonMember);
+    }
+    // TODO metadatas
+    Json::StreamWriterBuilder wbuilder;
+    wbuilder["commentStyle"] = "None";
+    wbuilder["indentation"] = "";
+    invite["application/invite+json"] = Json::writeString(wbuilder, root);
+    return invite;
+}
+
 } // namespace jami
\ No newline at end of file
diff --git a/src/jamidht/conversation.h b/src/jamidht/conversation.h
index 075415e4d5bec2e4c24f0c4b502c2683911df99c..1bea272328fd4d95625874cb17453bc42d46b456 100644
--- a/src/jamidht/conversation.h
+++ b/src/jamidht/conversation.h
@@ -49,6 +49,7 @@ public:
     std::string addMember(const std::string& contactUri);
     bool removeMember(const std::string& contactUri);
     /**
+     * @param includeInvited        If we want invited members
      * @return a vector of member details:
      * {
      *  "uri":"xxx",
@@ -57,7 +58,7 @@ public:
      *  ...
      * }
      */
-    std::vector<std::map<std::string, std::string>> getMembers();
+    std::vector<std::map<std::string, std::string>> getMembers(bool includeInvited = false) const;
 
     /**
      * Join a conversation
@@ -107,6 +108,12 @@ public:
      */
     bool mergeHistory(const std::string& uri);
 
+    /**
+     * Generate an invitation to send to new contacts
+     * @return the invite to send
+     */
+    std::map<std::string, std::string> generateInvitation() const;
+
 private:
     class Impl;
     std::unique_ptr<Impl> pimpl_;
diff --git a/src/jamidht/jamiaccount.cpp b/src/jamidht/jamiaccount.cpp
index 7bfd6dfb1df869271e74aefcc533182a9786dbb9..5df2da730d472d97d121f8aad396924bf40b2198 100644
--- a/src/jamidht/jamiaccount.cpp
+++ b/src/jamidht/jamiaccount.cpp
@@ -87,7 +87,6 @@
 #include <opendht/http.h>
 
 #include <yaml-cpp/yaml.h>
-#include <json/json.h>
 
 #include <unistd.h>
 
@@ -162,6 +161,44 @@ struct ConvInfo
     MSGPACK_DEFINE_MAP(id, created, removed)
 };
 
+// ConversationRequest
+ConversationRequest::ConversationRequest(const Json::Value& json)
+{
+    received = json["received"].asLargestUInt();
+    declined = json["declined"].asLargestUInt();
+    from = json["from"].asString();
+    conversationId = json["conversationId"].asString();
+    auto& md = json["metadatas"];
+    for (const auto& member : md.getMemberNames()) {
+        metadatas.emplace(member, md[member].asString());
+    }
+}
+
+Json::Value
+ConversationRequest::toJson() const
+{
+    Json::Value json;
+    json["conversationId"] = conversationId;
+    json["from"] = from;
+    json["received"] = static_cast<uint32_t>(received);
+    if (declined)
+        json["declined"] = static_cast<uint32_t>(declined);
+    for (const auto& [key, value] : metadatas) {
+        json["metadatas"][key] = value;
+    }
+    return json;
+}
+
+std::map<std::string, std::string>
+ConversationRequest::toMap() const
+{
+    auto result = metadatas;
+    result["id"] = conversationId;
+    result["from"] = from;
+    result["received"] = std::to_string(received);
+    return result;
+}
+
 namespace Migration {
 
 enum class State { // Contains all the Migration states
@@ -1977,6 +2014,7 @@ JamiAccount::doRegister()
         }
     }
     loadConvInfos();
+    loadConvRequests();
     JAMI_INFO("[Account %s] Conversations loaded!", getAccountID().c_str());
 
     // invalid state transitions:
@@ -2583,34 +2621,42 @@ JamiAccount::doRegister_()
         if (!dhtPeerConnector_)
             dhtPeerConnector_ = std::make_unique<DhtPeerConnector>(*this);
 
-        dht_->listen<ConversationRequest>(
-            inboxDeviceKey, [this, inboxDeviceKey](ConversationRequest&& req) {
-                // TODO it's a trust request, we need to confirm incoming device
-                JAMI_INFO("Receive a new conversation request for conversation %s",
-                          req.conversationId.c_str());
-                auto convId = req.conversationId;
-                std::map<std::string, std::string> metadatas = req.metadatas;
-                {
-                    std::lock_guard<std::mutex> lk(conversationsRequestsMtx_);
-                    auto it = conversationsRequests_.find(convId);
-                    if (it != conversationsRequests_.end()) {
-                        JAMI_INFO("Received a request for a conversation already existing. Ignore");
-                        return true;
+        {
+            std::lock_guard<std::mutex> lock(buddyInfoMtx);
+            for (auto& buddy : trackedBuddies_) {
+                buddy.second.devices_cnt = 0;
+                trackPresence(buddy.first, buddy.second);
+            }
+        }
+
+        if (needsConvSync_.exchange(false)) {
+            // Sync conversations (at startup or when re-enabled)
+            std::lock_guard<std::mutex> lk(conversationsMtx_);
+            for (const auto& item : conversations_) {
+                const auto& convId = item.first;
+                const auto& conv = item.second;
+                if (!conv)
+                    continue;
+                for (const auto& member : conv->getMembers()) {
+                    auto peer = member.at("uri");
+                    if (username_.find(peer) != std::string::npos) {
+                        // TODO should be handled by device sync
+                        continue;
                     }
-                    conversationsRequests_[convId] = std::move(req);
+                    accountManager_->forEachDevice(
+                        dht::InfoHash(peer), [w = weak(), peer, convId](const dht::InfoHash& dev) {
+                            auto shared = w.lock();
+                            if (!shared)
+                                return;
+                            JAMI_INFO("[Account %s] Start syncing conversation %s with %s (%s)",
+                                      shared->getAccountID().c_str(),
+                                      convId.c_str(),
+                                      peer.c_str(),
+                                      dev.toString().c_str());
+                            shared->fetchNewCommits(peer, dev.toString(), convId);
+                        });
                 }
-                // TODO: store request to be persistent when restarting
-
-                emitSignal<DRing::ConversationSignal::ConversationRequestReceived>(accountID_,
-                                                                                   convId,
-                                                                                   metadatas);
-                return true;
-            });
-
-        std::lock_guard<std::mutex> lock(buddyInfoMtx);
-        for (auto& buddy : trackedBuddies_) {
-            buddy.second.devices_cnt = 0;
-            trackPresence(buddy.first, buddy.second);
+            }
         }
     } catch (const std::exception& e) {
         JAMI_ERR("Error registering DHT account: %s", e.what());
@@ -2788,8 +2834,10 @@ JamiAccount::doUnregister(std::function<void(bool)> released_cb)
 
     // Stop all current p2p connections if account is disabled
     // Else, we let the system managing if the co is down or not
-    if (not isEnabled())
+    if (not isEnabled()) {
+        needsConvSync_ = true;
         shutdownConnections();
+    }
 
     dht_->join();
 
@@ -3803,7 +3851,6 @@ JamiAccount::startConversation()
 void
 JamiAccount::acceptConversationRequest(const std::string& conversationId)
 {
-    // TODO DRT to optimize connections
     // For all conversation members, try to open a git channel with this conversation ID
     std::unique_lock<std::mutex> lk(conversationsRequestsMtx_);
     auto request = conversationsRequests_.find(conversationId);
@@ -3815,41 +3862,39 @@ JamiAccount::acceptConversationRequest(const std::string& conversationId)
         std::lock_guard<std::mutex> lk(pendingConversationsFetchMtx_);
         pendingConversationsFetch_[request->first] = PendingConversationFetch {};
     }
-    for (const auto& member : request->second.members) {
-        auto memberHash = dht::InfoHash(member);
-        if (!memberHash) {
-            // TODO check why some members are 000000
-            continue;
-        }
-        // TODO cf sync between devices
-        forEachDevice(memberHash, [this, request = request->second](const dht::InfoHash& dev) {
-            if (dev == dht()->getId())
-                return;
-            std::string channelName = "git://" + dev.toString() + "/" + request.conversationId;
-            connectionManager().connectDevice(
-                dev,
-                channelName,
-                [this, request](std::shared_ptr<ChannelSocket> socket, const DeviceId& dev) {
-                    if (socket) {
-                        std::unique_lock<std::mutex> lk(pendingConversationsFetchMtx_);
-                        auto& pending = pendingConversationsFetch_[request.conversationId];
-                        if (!pending.ready) {
-                            pending.ready = true;
-                            pending.deviceId = dev.toString();
-                            lk.unlock();
-                            // Save the git socket
-                            addGitSocket(dev.toString(), request.conversationId, socket);
-                            // TODO when do we remove the gitSocket?
-                        } else {
-                            lk.unlock();
-                            socket->shutdown();
-                        }
-                    }
-                });
-        });
+    auto memberHash = dht::InfoHash(request->second.from);
+    if (!memberHash) {
+        JAMI_WARN("Invalid member detected");
+        return;
     }
+    forEachDevice(memberHash, [this, request = request->second](const dht::InfoHash& dev) {
+        if (dev == dht()->getId())
+            return;
+        connectionManager().connectDevice(
+            dev,
+            "git://" + dev.toString() + "/" + request.conversationId,
+            [this, request](std::shared_ptr<ChannelSocket> socket, const DeviceId& dev) {
+                if (socket) {
+                    std::unique_lock<std::mutex> lk(pendingConversationsFetchMtx_);
+                    auto& pending = pendingConversationsFetch_[request.conversationId];
+                    if (!pending.ready) {
+                        pending.ready = true;
+                        pending.deviceId = dev.toString();
+                        lk.unlock();
+                        // Save the git socket
+                        addGitSocket(dev.toString(), request.conversationId, socket);
+                        // TODO when do we remove the gitSocket?
+                    } else {
+                        lk.unlock();
+                        socket->shutdown();
+                    }
+                }
+            });
+    });
     conversationsRequests_.erase(conversationId);
     lk.unlock();
+    saveConvRequests();
+    syncWithConnected();
     checkConversationsEvents();
 }
 
@@ -3911,7 +3956,16 @@ JamiAccount::handlePendingConversations()
 
 void
 JamiAccount::declineConversationRequest(const std::string& conversationId)
-{}
+{
+    std::unique_lock<std::mutex> lk(conversationsRequestsMtx_);
+    auto request = conversationsRequests_.find(conversationId);
+    if (request == conversationsRequests_.end())
+        return;
+    request->second.declined = std::time(nullptr);
+    lk.unlock();
+    saveConvRequests();
+    syncWithConnected();
+}
 
 bool
 JamiAccount::removeConversation(const std::string& conversationId)
@@ -3934,35 +3988,45 @@ JamiAccount::getConversations()
 std::vector<std::map<std::string, std::string>>
 JamiAccount::getConversationRequests()
 {
-    // TODO
-    return {};
+    std::vector<std::map<std::string, std::string>> requests;
+    {
+        std::lock_guard<std::mutex> lk(conversationsRequestsMtx_);
+        requests.reserve(conversationsRequests_.size());
+        for (const auto& [id, request] : conversationsRequests_) {
+            if (request.declined)
+                continue; // Do not add declined requests
+            requests.emplace_back(request.toMap());
+        }
+    }
+    return requests;
 }
 
 // Member management
 bool
-JamiAccount::addConversationMember(const std::string& conversationId, const std::string& contactUri)
+JamiAccount::addConversationMember(const std::string& conversationId,
+                                   const std::string& contactUri,
+                                   bool sendRequest)
 {
     std::lock_guard<std::mutex> lk(conversationsMtx_);
     // Add a new member in the conversation
-    if (conversations_[conversationId]->addMember(contactUri).empty()) {
+    auto it = conversations_.find(conversationId);
+    if (it == conversations_.end()) {
+        JAMI_ERR("Conversation %s doesn't exist", conversationId.c_str());
+        return false;
+    }
+    auto commitId = it->second->addMember(contactUri);
+    if (commitId.empty()) {
         JAMI_WARN("Couldn't add %s to %s", contactUri.c_str(), conversationId.c_str());
         return false;
     }
-    // Invite the new member to the conversation
-    auto toH = dht::InfoHash(contactUri);
-    ConversationRequest req;
-    req.conversationId = conversationId;
-    auto convMembers = conversations_[conversationId]->getMembers();
-    for (const auto& member : convMembers)
-        req.members.emplace_back(member.at("uri"));
-    req.metadatas = {/* TODO */};
-    // TODO message engine
-    forEachDevice(toH, [this, toH, req](const dht::InfoHash& dev) {
-        JAMI_INFO("Sending conversation invite %s / %s",
-                  toH.toString().c_str(),
-                  dev.toString().c_str());
-        dht_->putEncrypted(dht::InfoHash::get("inbox:" + dev.toString()), dev, req);
-    });
+    auto messages = it->second->loadMessages(commitId, 1);
+    if (messages.empty())
+        return false; // should not happen
+    emitSignal<DRing::ConversationSignal::MessageReceived>(getAccountID(),
+                                                           conversationId,
+                                                           messages.front());
+    if (sendRequest)
+        sendTextMessage(contactUri, it->second->generateInvitation());
     return true;
 }
 
@@ -3981,7 +4045,7 @@ JamiAccount::getConversationMembers(const std::string& conversationId)
     std::lock_guard<std::mutex> lk(conversationsMtx_);
     auto conversation = conversations_.find(conversationId);
     if (conversation != conversations_.end() && conversation->second)
-        return conversation->second->getMembers();
+        return conversation->second->getMembers(true);
     return {};
 }
 
@@ -3990,7 +4054,8 @@ void
 JamiAccount::sendMessage(const std::string& conversationId,
                          const std::string& message,
                          const std::string& parent,
-                         const std::string& type)
+                         const std::string& type,
+                         bool announce)
 {
     std::lock_guard<std::mutex> lk(conversationsMtx_);
     auto conversation = conversations_.find(conversationId);
@@ -4015,7 +4080,8 @@ JamiAccount::sendMessage(const std::string& conversationId,
                 if (username_.find(uri) != std::string::npos)
                     continue;
                 // Announce to all members that a new message is sent
-                sendTextMessage(uri, {{"application/im-gitmessage-id", text}});
+                if (announce)
+                    sendTextMessage(uri, {{"application/im-gitmessage-id", text}});
             }
         } else {
             JAMI_ERR("Failed to send message to conversation %s", conversationId.c_str());
@@ -4058,6 +4124,14 @@ JamiAccount::onNewGitCommit(const std::string& peer,
              peer.c_str(),
              conversationId.c_str(),
              commitId.c_str());
+    fetchNewCommits(peer, deviceId, conversationId);
+}
+
+void
+JamiAccount::fetchNewCommits(const std::string& peer,
+                             const std::string& deviceId,
+                             const std::string& conversationId)
+{
     auto conversation = conversations_.find(conversationId);
     if (conversation != conversations_.end() && conversation->second) {
         if (!conversation->second->isMember(peer)) {
@@ -4141,10 +4215,53 @@ JamiAccount::onNewGitCommit(const std::string& peer,
                 });
         }
     } else {
+        {
+            // Check if the conversation is still a request
+            std::lock_guard<std::mutex> lk(conversationsRequestsMtx_);
+            if (conversationsRequests_.find(conversationId) != conversationsRequests_.end())
+                return;
+        }
+        {
+            // Check if the conversation is cloning
+            std::lock_guard<std::mutex> lk(pendingConversationsFetchMtx_);
+            if (pendingConversationsFetch_.find(conversationId) != pendingConversationsFetch_.end())
+                return;
+        }
         JAMI_WARN("Could not find conversation %s", conversationId.c_str());
     }
 }
 
+void
+JamiAccount::onConversationRequest(const std::string& from, const Json::Value& value)
+{
+    ConversationRequest req(value);
+    JAMI_INFO("[Account %s] Receive a new conversation request for conversation %s",
+              getAccountID().c_str(),
+              req.conversationId.c_str());
+    auto convId = req.conversationId;
+    req.from = from;
+
+    {
+        std::lock_guard<std::mutex> lk(conversationsRequestsMtx_);
+        auto it = conversationsRequests_.find(convId);
+        if (it != conversationsRequests_.end()) {
+            JAMI_INFO("[Account %s] Received a request for a conversation already existing. "
+                      "Ignore",
+                      getAccountID().c_str());
+            return;
+        }
+        req.received = std::time(nullptr);
+        conversationsRequests_[convId] = req;
+    }
+    saveConvRequests();
+    // Note: no need to sync here because over connected devices should receives
+    // the same conversation request. Will sync when the conversation will be added
+
+    emitSignal<DRing::ConversationSignal::ConversationRequestReceived>(accountID_,
+                                                                       convId,
+                                                                       req.toMap());
+}
+
 void
 JamiAccount::cacheTurnServers()
 {
@@ -4566,6 +4683,10 @@ JamiAccount::cacheSyncConnection(std::shared_ptr<ChannelSocket>&& socket,
             for (const auto& jsonConv : value["conversations"]) {
                 auto convId = jsonConv["id"].asString();
                 auto removed = jsonConv.isMember("removed");
+                {
+                    std::lock_guard<std::mutex> lk(conversationsRequestsMtx_);
+                    conversationsRequests_.erase(convId);
+                }
                 if (not removed) {
                     if (!isConversation(convId)) {
                         {
@@ -4617,8 +4738,39 @@ JamiAccount::cacheSyncConnection(std::shared_ptr<ChannelSocket>&& socket,
                     }
                 }
             }
-            saveConvInfos();
         }
+
+        if (value.isMember("conversationsRequests")) {
+            for (const auto& jsonReq : value["conversationsRequests"]) {
+                auto convId = jsonReq["conversationId"].asString();
+                if (isConversation(convId)) {
+                    // Already accepted request
+                    std::lock_guard<std::mutex> lk(conversationsRequestsMtx_);
+                    conversationsRequests_.erase(convId);
+                    continue;
+                }
+
+                // New request
+                ConversationRequest req(jsonReq);
+                {
+                    std::lock_guard<std::mutex> lk(conversationsRequestsMtx_);
+                    conversationsRequests_[convId] = req;
+                }
+
+                if (req.declined != 0)
+                    continue; // Request removed, do not emit signal
+
+                JAMI_INFO("[Account %s] New request detected for conversation %s (device %s)",
+                          getAccountID().c_str(),
+                          convId.c_str(),
+                          deviceId.c_str());
+                emitSignal<DRing::ConversationSignal::ConversationRequestReceived>(getAccountID(),
+                                                                                   convId,
+                                                                                   req.toMap());
+            }
+        }
+        saveConvInfos();
+        saveConvRequests();
         return len;
     });
 }
@@ -4657,12 +4809,15 @@ void
 JamiAccount::syncInfos(const std::shared_ptr<ChannelSocket>& socket)
 {
     // Sync conversations
-    if (not socket or convInfos_.empty())
+    if (not socket or (convInfos_.empty() and conversationsRequests_.empty()))
         return;
     Json::Value syncValue;
     for (const auto& info : convInfos_) {
         syncValue["conversations"].append(info.toJson());
     }
+    for (const auto& [_id, request] : conversationsRequests_) {
+        syncValue["conversationsRequests"].append(request.toJson());
+    }
 
     Json::StreamWriterBuilder builder;
     const auto sync = Json::writeString(builder, syncValue);
@@ -4707,4 +4862,28 @@ JamiAccount::saveConvInfos() const
     msgpack::pack(file, convInfos_);
 }
 
+void
+JamiAccount::loadConvRequests()
+{
+    try {
+        // read file
+        auto file = fileutils::loadFile("convRequests", idPath_);
+        // load values
+        msgpack::object_handle oh = msgpack::unpack((const char*) file.data(), file.size());
+        std::lock_guard<std::mutex> lk(conversationsRequestsMtx_);
+        oh.get().convert(conversationsRequests_);
+    } catch (const std::exception& e) {
+        JAMI_WARN("Error loading conversationsRequests: %s", e.what());
+    }
+}
+
+void
+JamiAccount::saveConvRequests()
+{
+    std::lock_guard<std::mutex> lk(conversationsRequestsMtx_);
+    std::ofstream file(idPath_ + DIR_SEPARATOR_STR "convRequests",
+                       std::ios::trunc | std::ios::binary);
+    msgpack::pack(file, conversationsRequests_);
+}
+
 } // namespace jami
diff --git a/src/jamidht/jamiaccount.h b/src/jamidht/jamiaccount.h
index 607926a043f8c629c242439604bf98d35ef650dd..a232911b04120bd1cdd80e018a99ef9685df3a11 100644
--- a/src/jamidht/jamiaccount.h
+++ b/src/jamidht/jamiaccount.h
@@ -53,6 +53,7 @@
 #include <chrono>
 #include <list>
 #include <future>
+#include <json/json.h>
 
 #if HAVE_RINGNS
 #include "namedirectory.h"
@@ -91,14 +92,22 @@ struct ConvInfo;
  * such as the conversation's vcard, etc. (TODO determine)
  * Transmitted via the UDP DHT
  */
-struct ConversationRequest : public dht::EncryptedValue<ConversationRequest>
+struct ConversationRequest
 {
-    static const constexpr dht::ValueType& TYPE = dht::ValueType::USER_DATA;
-    dht::Value::Id id = dht::Value::INVALID_ID;
     std::string conversationId;
-    std::vector<std::string> members;
+    std::string from;
     std::map<std::string, std::string> metadatas;
-    MSGPACK_DEFINE_MAP(id, conversationId, members, metadatas)
+
+    time_t received {0};
+    time_t declined {0};
+
+    ConversationRequest() = default;
+    ConversationRequest(const Json::Value& json);
+
+    Json::Value toJson() const;
+    std::map<std::string, std::string> toMap() const;
+
+    MSGPACK_DEFINE_MAP(conversationId, metadatas, received, declined)
 };
 
 using SipConnectionKey = std::pair<std::string /* accountId */, DeviceId>;
@@ -506,7 +515,9 @@ public:
     std::vector<std::map<std::string, std::string>> getConversationRequests();
 
     // Member management
-    bool addConversationMember(const std::string& conversationId, const std::string& contactUri);
+    bool addConversationMember(const std::string& conversationId,
+                               const std::string& contactUri,
+                               bool sendRequest = true);
     bool removeConversationMember(const std::string& conversationId, const std::string& contactUri);
     std::vector<std::map<std::string, std::string>> getConversationMembers(
         const std::string& conversationId);
@@ -515,7 +526,8 @@ public:
     void sendMessage(const std::string& conversationId,
                      const std::string& message,
                      const std::string& parent = "",
-                     const std::string& type = "text/plain");
+                     const std::string& type = "text/plain",
+                     bool announce = true);
     uint32_t loadConversationMessages(const std::string& conversationId,
                                       const std::string& fromMessage = "",
                                       size_t n = 0);
@@ -525,6 +537,12 @@ public:
                         const std::string& deviceId,
                         const std::string& conversationId,
                         const std::string& commitId) override;
+    void fetchNewCommits(const std::string& peer,
+                         const std::string& deviceId,
+                         const std::string& conversationId);
+
+    // Invites
+    void onConversationRequest(const std::string& from, const Json::Value&) override;
 
 private:
     NON_COPYABLE(JamiAccount);
@@ -669,6 +687,9 @@ private:
     void loadConvInfos();
     void saveConvInfos() const;
 
+    void loadConvRequests();
+    void saveConvRequests();
+
     template<class... Args>
     std::shared_ptr<IceTransport> createIceTransport(const Args&... args);
     void newOutgoingCallHelper(const std::shared_ptr<SIPCall>& call, std::string_view toUri);
@@ -912,6 +933,7 @@ private:
     void syncWith(const std::string& deviceId, const std::shared_ptr<ChannelSocket>& socket);
     void syncInfos(const std::shared_ptr<ChannelSocket>& socket);
     void syncWithConnected();
+    std::atomic_bool needsConvSync_ {true};
 };
 
 static inline std::ostream&
diff --git a/src/sip/sipaccountbase.cpp b/src/sip/sipaccountbase.cpp
index 8267108dba00dddde7a1643e6db2d57e8000b2cd..3e8bf5b7726d63dbaf0992c4025dd71b8be06bf5 100644
--- a/src/sip/sipaccountbase.cpp
+++ b/src/sip/sipaccountbase.cpp
@@ -58,6 +58,7 @@ namespace jami {
 
 static constexpr const char MIME_TYPE_IMDN[] {"message/imdn+xml"};
 static constexpr const char MIME_TYPE_GIT[] {"application/im-gitmessage-id"};
+static constexpr const char MIME_TYPE_INVITE_JSON[] {"application/invite+json"};
 static constexpr const char MIME_TYPE_IM_COMPOSING[] {"application/im-iscomposing+xml"};
 static constexpr std::chrono::steady_clock::duration COMPOSING_TIMEOUT {std::chrono::seconds(12)};
 
@@ -580,6 +581,16 @@ SIPAccountBase::onTextMessage(const std::string& id,
                            json["deviceId"].asString(),
                            json["id"].asString(),
                            json["commit"].asString());
+        } else if (m.first == MIME_TYPE_INVITE_JSON) {
+            Json::Value json;
+            std::string err;
+            Json::CharReaderBuilder rbuilder;
+            auto reader = std::unique_ptr<Json::CharReader>(rbuilder.newCharReader());
+            if (!reader->parse(m.second.data(), m.second.data() + m.second.size(), &json, &err)) {
+                JAMI_ERR("Can't parse server response: %s", err.c_str());
+                return;
+            }
+            onConversationRequest(from, json);
         }
     }
 
diff --git a/test/unitTest/conversation/conversation.cpp b/test/unitTest/conversation/conversation.cpp
index 6326cd5acfed0260fb392b79271b3fe462b11771..b6259fac94b6c6aad86bfac0ea2ce8798042e72f 100644
--- a/test/unitTest/conversation/conversation.cpp
+++ b/test/unitTest/conversation/conversation.cpp
@@ -24,6 +24,7 @@
 #include <string>
 #include <fstream>
 #include <streambuf>
+#include <git2.h>
 #include <filesystem>
 
 #include "manager.h"
@@ -35,8 +36,6 @@
 #include "fileutils.h"
 #include "account_const.h"
 
-#include <git2.h>
-
 using namespace std::string_literals;
 using namespace DRing::Account;
 
@@ -53,23 +52,34 @@ public:
 
     std::string aliceId;
     std::string bobId;
+    std::string carlaId;
 
 private:
     void testCreateConversation();
     void testGetConversation();
     void testAddMember();
+    void testAddOfflineMemberThenConnects();
     void testGetMembers();
     void testSendMessage();
     void testSendMessageTriggerMessageReceived();
     void testMergeTwoDifferentHeads();
+    void testGetRequests();
+    void testDeclineRequest();
+    void testSendMessageToMultipleParticipants();
+    void testPingPongMessages();
 
     CPPUNIT_TEST_SUITE(ConversationTest);
     CPPUNIT_TEST(testCreateConversation);
     CPPUNIT_TEST(testGetConversation);
     CPPUNIT_TEST(testAddMember);
+    CPPUNIT_TEST(testAddOfflineMemberThenConnects);
     CPPUNIT_TEST(testGetMembers);
     CPPUNIT_TEST(testSendMessage);
     CPPUNIT_TEST(testSendMessageTriggerMessageReceived);
+    CPPUNIT_TEST(testGetRequests);
+    CPPUNIT_TEST(testDeclineRequest);
+    CPPUNIT_TEST(testSendMessageToMultipleParticipants);
+    CPPUNIT_TEST(testPingPongMessages);
     CPPUNIT_TEST_SUITE_END();
 };
 
@@ -103,9 +113,21 @@ ConversationTest::setUp()
     details[ConfProperties::ARCHIVE_PATH] = "";
     bobId = Manager::instance().addAccount(details);
 
+    details = DRing::getAccountTemplate("RING");
+    details[ConfProperties::TYPE] = "RING";
+    details[ConfProperties::DISPLAYNAME] = "CARLA";
+    details[ConfProperties::ALIAS] = "CARLA";
+    details[ConfProperties::UPNP_ENABLED] = "true";
+    details[ConfProperties::ARCHIVE_PASSWORD] = "";
+    details[ConfProperties::ARCHIVE_PIN] = "";
+    details[ConfProperties::ARCHIVE_PATH] = "";
+    carlaId = Manager::instance().addAccount(details);
+
     JAMI_INFO("Initialize account...");
     auto aliceAccount = Manager::instance().getAccount<JamiAccount>(aliceId);
     auto bobAccount = Manager::instance().getAccount<JamiAccount>(bobId);
+    auto carlaAccount = Manager::instance().getAccount<JamiAccount>(carlaId);
+    Manager::instance().sendRegister(carlaId, false);
     std::map<std::string, std::shared_ptr<DRing::CallbackWrapperBase>> confHandlers;
     std::mutex mtx;
     std::unique_lock<std::mutex> lk {mtx};
@@ -132,9 +154,10 @@ ConversationTest::tearDown()
     auto currentAccSize = Manager::instance().getAccountList().size();
     Manager::instance().removeAccount(aliceId, true);
     Manager::instance().removeAccount(bobId, true);
+    Manager::instance().removeAccount(carlaId, true);
     // Because cppunit is not linked with dbus, just poll if removed
     for (int i = 0; i < 40; ++i) {
-        if (Manager::instance().getAccountList().size() <= currentAccSize - 2)
+        if (Manager::instance().getAccountList().size() <= currentAccSize - 3)
             break;
         std::this_thread::sleep_for(std::chrono::milliseconds(100));
     }
@@ -258,6 +281,50 @@ ConversationTest::testAddMember()
     CPPUNIT_ASSERT(!fileutils::isFile(bobInvited));
 }
 
+void
+ConversationTest::testAddOfflineMemberThenConnects()
+{
+    auto aliceAccount = Manager::instance().getAccount<JamiAccount>(aliceId);
+    auto carlaAccount = Manager::instance().getAccount<JamiAccount>(carlaId);
+    auto carlaUri = carlaAccount->getUsername();
+    aliceAccount->trackBuddyPresence(carlaUri, true);
+    auto convId = aliceAccount->startConversation();
+
+    std::mutex mtx;
+    std::unique_lock<std::mutex> lk {mtx};
+    std::condition_variable cv;
+    std::map<std::string, std::shared_ptr<DRing::CallbackWrapperBase>> confHandlers;
+    bool conversationReady = false, requestReceived = false;
+    confHandlers.insert(DRing::exportable_callback<DRing::ConversationSignal::ConversationReady>(
+        [&](const std::string& accountId, const std::string& /* conversationId */) {
+            if (accountId == carlaId) {
+                conversationReady = true;
+                cv.notify_one();
+            }
+        }));
+    confHandlers.insert(
+        DRing::exportable_callback<DRing::ConversationSignal::ConversationRequestReceived>(
+            [&](const std::string& /*accountId*/,
+                const std::string& /* conversationId */,
+                std::map<std::string, std::string> /*metadatas*/) {
+                requestReceived = true;
+                cv.notify_one();
+            }));
+    DRing::registerSignalHandlers(confHandlers);
+
+    CPPUNIT_ASSERT(aliceAccount->addConversationMember(convId, carlaUri));
+    Manager::instance().sendRegister(carlaId, true);
+    cv.wait_for(lk, std::chrono::seconds(60));
+    CPPUNIT_ASSERT(requestReceived);
+
+    carlaAccount->acceptConversationRequest(convId);
+    cv.wait_for(lk, std::chrono::seconds(30));
+    CPPUNIT_ASSERT(conversationReady);
+    auto clonedPath = fileutils::get_data_dir() + DIR_SEPARATOR_STR + carlaAccount->getAccountID()
+                      + DIR_SEPARATOR_STR + "conversations" + DIR_SEPARATOR_STR + convId;
+    CPPUNIT_ASSERT(fileutils::isDirectory(clonedPath));
+}
+
 void
 ConversationTest::testGetMembers()
 {
@@ -308,9 +375,11 @@ ConversationTest::testGetMembers()
     CPPUNIT_ASSERT(fileutils::isDirectory(repoPath));
 
     auto members = aliceAccount->getConversationMembers(convId);
-    CPPUNIT_ASSERT(members.size() == 1);
+    CPPUNIT_ASSERT(members.size() == 2);
     CPPUNIT_ASSERT(members[0]["uri"] == aliceAccount->getUsername());
     CPPUNIT_ASSERT(members[0]["role"] == "admin");
+    CPPUNIT_ASSERT(members[1]["uri"] == bobUri);
+    CPPUNIT_ASSERT(members[1]["role"] == "invited");
 
     cv.wait_for(lk, std::chrono::seconds(30), [&]() { return requestReceived; });
     messageReceived = false;
@@ -321,9 +390,10 @@ ConversationTest::testGetMembers()
     cv.wait_for(lk, std::chrono::seconds(60), [&]() { return messageReceived; });
     members = aliceAccount->getConversationMembers(convId);
     CPPUNIT_ASSERT(members.size() == 2);
-    auto hasBob = members[0]["uri"] == bobUri || members[1]["uri"] == bobUri;
-    auto hasAlice = members[0]["uri"] == aliceUri || members[1]["uri"] == aliceUri;
-    CPPUNIT_ASSERT(hasAlice && hasBob);
+    CPPUNIT_ASSERT(members[0]["uri"] == aliceAccount->getUsername());
+    CPPUNIT_ASSERT(members[0]["role"] == "admin");
+    CPPUNIT_ASSERT(members[1]["uri"] == bobUri);
+    CPPUNIT_ASSERT(members[1]["role"] == "member");
 }
 
 void
@@ -394,7 +464,6 @@ void
 ConversationTest::testSendMessageTriggerMessageReceived()
 {
     auto aliceAccount = Manager::instance().getAccount<JamiAccount>(aliceId);
-
     std::mutex mtx;
     std::unique_lock<std::mutex> lk {mtx};
     std::condition_variable cv;
@@ -486,6 +555,243 @@ ConversationTest::testMergeTwoDifferentHeads()
     DRing::unregisterSignalHandlers();
 }
 
+void
+ConversationTest::testGetRequests()
+{
+    auto aliceAccount = Manager::instance().getAccount<JamiAccount>(aliceId);
+    auto bobAccount = Manager::instance().getAccount<JamiAccount>(bobId);
+    auto bobUri = bobAccount->getUsername();
+    std::mutex mtx;
+    std::unique_lock<std::mutex> lk {mtx};
+    std::condition_variable cv;
+    std::map<std::string, std::shared_ptr<DRing::CallbackWrapperBase>> confHandlers;
+    bool requestReceived = false;
+    confHandlers.insert(
+        DRing::exportable_callback<DRing::ConversationSignal::ConversationRequestReceived>(
+            [&](const std::string& /*accountId*/,
+                const std::string& /* conversationId */,
+                std::map<std::string, std::string> /*metadatas*/) {
+                requestReceived = true;
+                cv.notify_one();
+            }));
+    DRing::registerSignalHandlers(confHandlers);
+
+    auto convId = aliceAccount->startConversation();
+
+    CPPUNIT_ASSERT(aliceAccount->addConversationMember(convId, bobUri));
+    cv.wait_for(lk, std::chrono::seconds(30));
+    CPPUNIT_ASSERT(requestReceived);
+
+    auto requests = bobAccount->getConversationRequests();
+    CPPUNIT_ASSERT(requests.size() == 1);
+    CPPUNIT_ASSERT(requests.front()["id"] == convId);
+}
+
+void
+ConversationTest::testDeclineRequest()
+{
+    auto aliceAccount = Manager::instance().getAccount<JamiAccount>(aliceId);
+    auto bobAccount = Manager::instance().getAccount<JamiAccount>(bobId);
+    auto bobUri = bobAccount->getUsername();
+
+    std::mutex mtx;
+    std::unique_lock<std::mutex> lk {mtx};
+    std::condition_variable cv;
+    std::map<std::string, std::shared_ptr<DRing::CallbackWrapperBase>> confHandlers;
+    bool requestReceived = false;
+    confHandlers.insert(
+        DRing::exportable_callback<DRing::ConversationSignal::ConversationRequestReceived>(
+            [&](const std::string& /*accountId*/,
+                const std::string& /* conversationId */,
+                std::map<std::string, std::string> /*metadatas*/) {
+                requestReceived = true;
+                cv.notify_one();
+            }));
+    DRing::registerSignalHandlers(confHandlers);
+
+    auto convId = aliceAccount->startConversation();
+
+    CPPUNIT_ASSERT(aliceAccount->addConversationMember(convId, bobUri));
+    cv.wait_for(lk, std::chrono::seconds(30));
+    CPPUNIT_ASSERT(requestReceived);
+
+    bobAccount->declineConversationRequest(convId);
+    // Decline request
+    auto requests = bobAccount->getConversationRequests();
+    CPPUNIT_ASSERT(requests.size() == 0);
+}
+
+void
+ConversationTest::testSendMessageToMultipleParticipants()
+{
+    auto aliceAccount = Manager::instance().getAccount<JamiAccount>(aliceId);
+    auto bobAccount = Manager::instance().getAccount<JamiAccount>(bobId);
+    auto bobUri = bobAccount->getUsername();
+    auto carlaAccount = Manager::instance().getAccount<JamiAccount>(carlaId);
+    auto carlaUri = carlaAccount->getUsername();
+    aliceAccount->trackBuddyPresence(carlaUri, true);
+
+    // Enable carla
+    std::map<std::string, std::shared_ptr<DRing::CallbackWrapperBase>> confHandlers;
+    std::mutex mtx;
+    std::unique_lock<std::mutex> lk {mtx};
+    std::condition_variable cv;
+    confHandlers.insert(
+        DRing::exportable_callback<DRing::ConfigurationSignal::VolatileDetailsChanged>(
+            [&](const std::string&, const std::map<std::string, std::string>&) {
+                auto details = carlaAccount->getVolatileAccountDetails();
+                auto daemonStatus = details[DRing::Account::ConfProperties::Registration::STATUS];
+                if (daemonStatus == "REGISTERED") {
+                    cv.notify_one();
+                }
+            }));
+    DRing::registerSignalHandlers(confHandlers);
+
+    Manager::instance().sendRegister(carlaId, true);
+    cv.wait_for(lk, std::chrono::seconds(30));
+    confHandlers.clear();
+    DRing::unregisterSignalHandlers();
+
+    auto messageReceivedAlice = 0;
+    auto messageReceivedBob = 0;
+    auto messageReceivedCarla = 0;
+    auto requestReceived = 0;
+    auto conversationReady = 0;
+    confHandlers.insert(DRing::exportable_callback<DRing::ConversationSignal::MessageReceived>(
+        [&](const std::string& accountId,
+            const std::string& /* conversationId */,
+            std::map<std::string, std::string> /*message*/) {
+            if (accountId == aliceId)
+                messageReceivedAlice += 1;
+            if (accountId == bobId)
+                messageReceivedBob += 1;
+            if (accountId == carlaId)
+                messageReceivedCarla += 1;
+            cv.notify_one();
+        }));
+
+    confHandlers.insert(
+        DRing::exportable_callback<DRing::ConversationSignal::ConversationRequestReceived>(
+            [&](const std::string& /*accountId*/,
+                const std::string& /* conversationId */,
+                std::map<std::string, std::string> /*metadatas*/) {
+                requestReceived += 1;
+                cv.notify_one();
+            }));
+
+    confHandlers.insert(DRing::exportable_callback<DRing::ConversationSignal::ConversationReady>(
+        [&](const std::string& /*accountId*/, const std::string& /* conversationId */) {
+            conversationReady += 1;
+            cv.notify_one();
+        }));
+    DRing::registerSignalHandlers(confHandlers);
+
+    auto convId = aliceAccount->startConversation();
+
+    CPPUNIT_ASSERT(aliceAccount->addConversationMember(convId, bobUri));
+    CPPUNIT_ASSERT(aliceAccount->addConversationMember(convId, carlaUri));
+    CPPUNIT_ASSERT(
+        cv.wait_for(lk, std::chrono::seconds(60), [&]() { return requestReceived == 2; }));
+
+    messageReceivedAlice = 0;
+    bobAccount->acceptConversationRequest(convId);
+    carlaAccount->acceptConversationRequest(convId);
+    // >= because we can have merges cause the accept commits
+    CPPUNIT_ASSERT(cv.wait_for(lk, std::chrono::seconds(60), [&]() {
+        return conversationReady == 3 && messageReceivedAlice >= 2;
+    }));
+
+    // Assert that repository exists
+    auto repoPath = fileutils::get_data_dir() + DIR_SEPARATOR_STR + bobAccount->getAccountID()
+                    + DIR_SEPARATOR_STR + "conversations" + DIR_SEPARATOR_STR + convId;
+    CPPUNIT_ASSERT(fileutils::isDirectory(repoPath));
+    repoPath = fileutils::get_data_dir() + DIR_SEPARATOR_STR + carlaAccount->getAccountID()
+               + DIR_SEPARATOR_STR + "conversations" + DIR_SEPARATOR_STR + convId;
+    CPPUNIT_ASSERT(fileutils::isDirectory(repoPath));
+
+    aliceAccount->sendMessage(convId, "hi");
+    CPPUNIT_ASSERT(cv.wait_for(lk, std::chrono::seconds(60), [&]() {
+        return messageReceivedBob >= 1 && messageReceivedCarla >= 1;
+    }));
+    DRing::unregisterSignalHandlers();
+}
+
+void
+ConversationTest::testPingPongMessages()
+{
+    auto aliceAccount = Manager::instance().getAccount<JamiAccount>(aliceId);
+    auto bobAccount = Manager::instance().getAccount<JamiAccount>(bobId);
+    auto bobUri = bobAccount->getUsername();
+    std::mutex mtx;
+    std::unique_lock<std::mutex> lk {mtx};
+    std::condition_variable cv;
+    std::map<std::string, std::shared_ptr<DRing::CallbackWrapperBase>> confHandlers;
+    auto messageBobReceived = 0, messageAliceReceived = 0;
+    bool requestReceived = false;
+    bool conversationReady = false;
+    confHandlers.insert(DRing::exportable_callback<DRing::ConversationSignal::MessageReceived>(
+        [&](const std::string& accountId,
+            const std::string& /* conversationId */,
+            std::map<std::string, std::string> /*message*/) {
+            if (accountId == bobId) {
+                messageBobReceived += 1;
+            }
+            if (accountId == aliceId) {
+                messageAliceReceived += 1;
+            }
+            if (messageAliceReceived == messageBobReceived)
+                cv.notify_one();
+        }));
+    confHandlers.insert(
+        DRing::exportable_callback<DRing::ConversationSignal::ConversationRequestReceived>(
+            [&](const std::string& /*accountId*/,
+                const std::string& /* conversationId */,
+                std::map<std::string, std::string> /*metadatas*/) {
+                requestReceived = true;
+                cv.notify_one();
+            }));
+    confHandlers.insert(DRing::exportable_callback<DRing::ConversationSignal::ConversationReady>(
+        [&](const std::string& accountId, const std::string& /* conversationId */) {
+            if (accountId == bobId) {
+                conversationReady = true;
+                cv.notify_one();
+            }
+        }));
+    DRing::registerSignalHandlers(confHandlers);
+    auto convId = aliceAccount->startConversation();
+    CPPUNIT_ASSERT(aliceAccount->addConversationMember(convId, bobUri));
+    cv.wait_for(lk, std::chrono::seconds(30));
+    CPPUNIT_ASSERT(requestReceived);
+    messageAliceReceived = 0;
+    bobAccount->acceptConversationRequest(convId);
+    CPPUNIT_ASSERT(cv.wait_for(lk, std::chrono::seconds(60), [&]() {
+        return conversationReady && messageAliceReceived == 1;
+    }));
+    // Assert that repository exists
+    auto repoPath = fileutils::get_data_dir() + DIR_SEPARATOR_STR + bobAccount->getAccountID()
+                    + DIR_SEPARATOR_STR + "conversations" + DIR_SEPARATOR_STR + convId;
+    CPPUNIT_ASSERT(fileutils::isDirectory(repoPath));
+    messageBobReceived = 0;
+    messageAliceReceived = 0;
+    aliceAccount->sendMessage(convId, "ping");
+    CPPUNIT_ASSERT(cv.wait_for(lk, std::chrono::seconds(30), [&]() {
+        return messageBobReceived == 1 && messageAliceReceived == 1;
+    }));
+    bobAccount->sendMessage(convId, "pong");
+    CPPUNIT_ASSERT(cv.wait_for(lk, std::chrono::seconds(30), [&]() {
+        return messageBobReceived == 2 && messageAliceReceived == 2;
+    }));
+    bobAccount->sendMessage(convId, "ping");
+    CPPUNIT_ASSERT(cv.wait_for(lk, std::chrono::seconds(30), [&]() {
+        return messageBobReceived == 3 && messageAliceReceived == 3;
+    }));
+    aliceAccount->sendMessage(convId, "pong");
+    CPPUNIT_ASSERT(cv.wait_for(lk, std::chrono::seconds(30), [&]() {
+        return messageBobReceived == 4 && messageAliceReceived == 4;
+    }));
+    DRing::unregisterSignalHandlers();
+}
+
 } // namespace test
 } // namespace jami
 
diff --git a/test/unitTest/syncHistory/syncHistory.cpp b/test/unitTest/syncHistory/syncHistory.cpp
index 1d5c8251f9b427aafa65346e851abf0067a02120..3f13dc284380867b72494f314b1ad9db4d04aac3 100644
--- a/test/unitTest/syncHistory/syncHistory.cpp
+++ b/test/unitTest/syncHistory/syncHistory.cpp
@@ -52,17 +52,20 @@ public:
     void tearDown();
 
     std::string aliceId;
+    std::string bobId;
     std::string alice2Id;
 
 private:
     void testCreateConversationThenSync();
     void testCreateConversationWithOnlineDevice();
     void testCreateConversationWithMessagesThenAddDevice();
+    void testReceivesInviteThenAddDevice();
 
     CPPUNIT_TEST_SUITE(SyncHistoryTest);
     CPPUNIT_TEST(testCreateConversationThenSync);
     CPPUNIT_TEST(testCreateConversationWithOnlineDevice);
     CPPUNIT_TEST(testCreateConversationWithMessagesThenAddDevice);
+    CPPUNIT_TEST(testReceivesInviteThenAddDevice);
     CPPUNIT_TEST_SUITE_END();
 };
 
@@ -81,8 +84,19 @@ SyncHistoryTest::setUp()
     details[ConfProperties::ARCHIVE_PATH] = "";
     aliceId = Manager::instance().addAccount(details);
 
+    details = DRing::getAccountTemplate("RING");
+    details[ConfProperties::TYPE] = "RING";
+    details[ConfProperties::DISPLAYNAME] = "BOB";
+    details[ConfProperties::ALIAS] = "BOB";
+    details[ConfProperties::UPNP_ENABLED] = "true";
+    details[ConfProperties::ARCHIVE_PASSWORD] = "";
+    details[ConfProperties::ARCHIVE_PIN] = "";
+    details[ConfProperties::ARCHIVE_PATH] = "";
+    bobId = Manager::instance().addAccount(details);
+
     JAMI_INFO("Initialize account...");
     auto aliceAccount = Manager::instance().getAccount<JamiAccount>(aliceId);
+    auto bobAccount = Manager::instance().getAccount<JamiAccount>(bobId);
     std::map<std::string, std::shared_ptr<DRing::CallbackWrapperBase>> confHandlers;
     std::mutex mtx;
     std::unique_lock<std::mutex> lk {mtx};
@@ -94,33 +108,43 @@ SyncHistoryTest::setUp()
                 auto details = aliceAccount->getVolatileAccountDetails();
                 auto daemonStatus = details[DRing::Account::ConfProperties::Registration::STATUS];
                 ready = (daemonStatus == "REGISTERED");
+                details = bobAccount->getVolatileAccountDetails();
+                daemonStatus = details[DRing::Account::ConfProperties::Registration::STATUS];
+                ready &= (daemonStatus == "REGISTERED");
                 if (ready)
                     cv.notify_one();
             }));
     DRing::registerSignalHandlers(confHandlers);
     cv.wait_for(lk, std::chrono::seconds(30));
     DRing::unregisterSignalHandlers();
+    alice2Id = "";
 }
 
 void
 SyncHistoryTest::tearDown()
 {
     JAMI_INFO("Remove created accounts...");
+    auto aliceArchive = std::filesystem::current_path().string() + "/alice.gz";
+    std::remove(aliceArchive.c_str());
 
     std::map<std::string, std::shared_ptr<DRing::CallbackWrapperBase>> confHandlers;
     std::mutex mtx;
     std::unique_lock<std::mutex> lk {mtx};
     std::condition_variable cv;
     auto currentAccSize = Manager::instance().getAccountList().size();
+    auto toRemove = alice2Id.empty() ? 2 : 3;
     confHandlers.insert(
         DRing::exportable_callback<DRing::ConfigurationSignal::AccountsChanged>([&]() {
-            if (Manager::instance().getAccountList().size() <= currentAccSize - 1) {
+            if (Manager::instance().getAccountList().size() <= currentAccSize - toRemove) {
                 cv.notify_one();
             }
         }));
     DRing::registerSignalHandlers(confHandlers);
 
     Manager::instance().removeAccount(aliceId, true);
+    if (!alice2Id.empty())
+        Manager::instance().removeAccount(alice2Id, true);
+    Manager::instance().removeAccount(bobId, true);
     // Because cppunit is not linked with dbus, just poll if removed
     cv.wait_for(lk, std::chrono::seconds(30));
 
@@ -180,25 +204,6 @@ SyncHistoryTest::testCreateConversationThenSync()
     DRing::unregisterSignalHandlers();
     confHandlers.clear();
 
-    // Remove alice 2 and alice.gz
-    std::remove(aliceArchive.c_str());
-
-    auto currentAccSize = Manager::instance().getAccountList().size();
-    confHandlers.insert(
-        DRing::exportable_callback<DRing::ConfigurationSignal::AccountsChanged>([&]() {
-            if (Manager::instance().getAccountList().size() <= currentAccSize - 1) {
-                cv.notify_one();
-            }
-        }));
-    DRing::registerSignalHandlers(confHandlers);
-
-    Manager::instance().removeAccount(alice2Id, true);
-    // Because cppunit is not linked with dbus, just poll if removed
-    cv.wait_for(lk, std::chrono::seconds(30));
-
-    DRing::unregisterSignalHandlers();
-    confHandlers.clear();
-
     // Check if conversation is ready
     CPPUNIT_ASSERT(conversationReady);
 }
@@ -256,25 +261,6 @@ SyncHistoryTest::testCreateConversationWithOnlineDevice()
     DRing::unregisterSignalHandlers();
     confHandlers.clear();
 
-    // Remove alice 2 and alice.gz
-    std::remove(aliceArchive.c_str());
-
-    auto currentAccSize = Manager::instance().getAccountList().size();
-    confHandlers.insert(
-        DRing::exportable_callback<DRing::ConfigurationSignal::AccountsChanged>([&]() {
-            if (Manager::instance().getAccountList().size() <= currentAccSize - 1) {
-                cv.notify_one();
-            }
-        }));
-    DRing::registerSignalHandlers(confHandlers);
-
-    Manager::instance().removeAccount(alice2Id, true);
-    // Because cppunit is not linked with dbus, just poll if removed
-    cv.wait_for(lk, std::chrono::seconds(30));
-
-    DRing::unregisterSignalHandlers();
-    confHandlers.clear();
-
     // Check if conversation is ready
     CPPUNIT_ASSERT(conversationReady);
 }
@@ -353,25 +339,76 @@ SyncHistoryTest::testCreateConversationWithMessagesThenAddDevice()
     CPPUNIT_ASSERT(messages[0]["body"] == "Message 3");
     CPPUNIT_ASSERT(messages[1]["body"] == "Message 2");
     CPPUNIT_ASSERT(messages[2]["body"] == "Message 1");
+}
+
+void
+SyncHistoryTest::testReceivesInviteThenAddDevice()
+{
+    auto aliceAccount = Manager::instance().getAccount<JamiAccount>(aliceId);
 
-    // Remove alice 2 and alice.gz
+    // Export alice
+    auto aliceArchive = std::filesystem::current_path().string() + "/alice.gz";
     std::remove(aliceArchive.c_str());
+    aliceAccount->exportArchive(aliceArchive);
 
-    auto currentAccSize = Manager::instance().getAccountList().size();
+    auto bobAccount = Manager::instance().getAccount<JamiAccount>(bobId);
+    auto uri = aliceAccount->getUsername();
+
+    // Start conversation for Alice
+    auto convId = bobAccount->startConversation();
+    bobAccount->addConversationMember(convId, uri);
+
+    // Check that alice receives the request
+    std::mutex mtx;
+    std::unique_lock<std::mutex> lk {mtx};
+    std::condition_variable cv;
+    std::map<std::string, std::shared_ptr<DRing::CallbackWrapperBase>> confHandlers;
+    auto requestReceived = false;
     confHandlers.insert(
-        DRing::exportable_callback<DRing::ConfigurationSignal::AccountsChanged>([&]() {
-            if (Manager::instance().getAccountList().size() <= currentAccSize - 1) {
-                cv.notify_one();
-            }
-        }));
+        DRing::exportable_callback<DRing::ConversationSignal::ConversationRequestReceived>(
+            [&](const std::string& accountId,
+                const std::string& conversationId,
+                std::map<std::string, std::string> /*metadatas*/) {
+                if (accountId == aliceId && conversationId == convId) {
+                    requestReceived = true;
+                    cv.notify_one();
+                }
+            }));
     DRing::registerSignalHandlers(confHandlers);
 
-    Manager::instance().removeAccount(alice2Id, true);
-    // Because cppunit is not linked with dbus, just poll if removed
     cv.wait_for(lk, std::chrono::seconds(30));
+    DRing::unregisterSignalHandlers();
+    confHandlers.clear();
+    CPPUNIT_ASSERT(requestReceived);
+
+    // Now create alice2
+    std::map<std::string, std::string> details = DRing::getAccountTemplate("RING");
+    details[ConfProperties::TYPE] = "RING";
+    details[ConfProperties::DISPLAYNAME] = "ALICE2";
+    details[ConfProperties::ALIAS] = "ALICE2";
+    details[ConfProperties::UPNP_ENABLED] = "true";
+    details[ConfProperties::ARCHIVE_PASSWORD] = "";
+    details[ConfProperties::ARCHIVE_PIN] = "";
+    details[ConfProperties::ARCHIVE_PATH] = aliceArchive;
+    alice2Id = Manager::instance().addAccount(details);
+
+    requestReceived = false;
+    confHandlers.insert(
+        DRing::exportable_callback<DRing::ConversationSignal::ConversationRequestReceived>(
+            [&](const std::string& accountId,
+                const std::string& conversationId,
+                std::map<std::string, std::string> /*metadatas*/) {
+                if (accountId == alice2Id && conversationId == convId) {
+                    requestReceived = true;
+                    cv.notify_one();
+                }
+            }));
+    DRing::registerSignalHandlers(confHandlers);
 
+    cv.wait_for(lk, std::chrono::seconds(30));
     DRing::unregisterSignalHandlers();
     confHandlers.clear();
+    CPPUNIT_ASSERT(requestReceived);
 }
 
 } // namespace test