diff --git a/bin/dbus/cx.ring.Ring.ConfigurationManager.xml b/bin/dbus/cx.ring.Ring.ConfigurationManager.xml
index 19e2a1e2799edb8e3f94f7301b24dfb283657963..15fab637c9a19a7fa82ef13c990768594d79d120 100644
--- a/bin/dbus/cx.ring.Ring.ConfigurationManager.xml
+++ b/bin/dbus/cx.ring.Ring.ConfigurationManager.xml
@@ -1911,6 +1911,29 @@
            </arg>
        </signal>
 
+       <signal name="conversationProfileUpdated" tp:name-for-bindings="conversationProfileUpdated">
+           <tp:added version="13.4.0"/>
+           <tp:docstring>
+               Notify clients when a conversation got its profile changed.
+           </tp:docstring>
+           <arg type="s" name="account_id">
+               <tp:docstring>
+                   Account id related
+               </tp:docstring>
+           </arg>
+           <arg type="s" name="conversation_id">
+               <tp:docstring>
+                   Conversation id
+               </tp:docstring>
+           </arg>
+           <annotation name="org.qtproject.QtDBus.QtTypeName.Out2" value="MapStringString"/>
+           <arg type="a{ss}" name="profile">
+               <tp:docstring>
+                    The new profile
+               </tp:docstring>
+           </arg>
+       </signal>
+
        <signal name="conversationRequestReceived" tp:name-for-bindings="conversationRequestReceived">
            <tp:added version="10.0.0"/>
            <tp:docstring>
diff --git a/bin/dbus/dbusclient.cpp b/bin/dbus/dbusclient.cpp
index 03f08c1490d3e768cd870f0d7cfd2744ea26199b..164bd6155d32d8371c8b2cc487eace0e58d8d009 100644
--- a/bin/dbus/dbusclient.cpp
+++ b/bin/dbus/dbusclient.cpp
@@ -301,6 +301,8 @@ DBusClient::initLibrary(int flags)
             bind(&DBusConfigurationManager::conversationLoaded, confM, _1, _2, _3, _4)),
         exportable_callback<ConversationSignal::MessageReceived>(
             bind(&DBusConfigurationManager::messageReceived, confM, _1, _2, _3)),
+        exportable_callback<ConversationSignal::ConversationProfileUpdated>(
+            bind(&DBusConfigurationManager::conversationProfileUpdated, confM, _1, _2, _3)),
         exportable_callback<ConversationSignal::ConversationRequestReceived>(
             bind(&DBusConfigurationManager::conversationRequestReceived, confM, _1, _2, _3)),
         exportable_callback<ConversationSignal::ConversationRequestDeclined>(
diff --git a/bin/jni/conversation.i b/bin/jni/conversation.i
index 7f457906b4df4b79e246e96bac285d085b521772..2485823942f735d85c1e2cce59a1e705e4d12c66 100644
--- a/bin/jni/conversation.i
+++ b/bin/jni/conversation.i
@@ -27,6 +27,7 @@ public:
     virtual ~ConversationCallback(){}
     virtual void conversationLoaded(uint32_t /* id */, const std::string& /*accountId*/, const std::string& /* conversationId */, std::vector<std::map<std::string, std::string>> /*messages*/){}
     virtual void messageReceived(const std::string& /*accountId*/, const std::string& /* conversationId */, std::map<std::string, std::string> /*message*/){}
+    virtual void conversationProfileUpdated(const std::string& /*accountId*/, const std::string& /* conversationId */, std::map<std::string, std::string> /*profile*/){}
     virtual void conversationRequestReceived(const std::string& /*accountId*/, const std::string& /* conversationId */, std::map<std::string, std::string> /*metadatas*/){}
     virtual void conversationRequestDeclined(const std::string& /*accountId*/, const std::string& /* conversationId */){}
     virtual void conversationReady(const std::string& /*accountId*/, const std::string& /* conversationId */){}
@@ -66,6 +67,7 @@ public:
     virtual ~ConversationCallback(){}
     virtual void conversationLoaded(uint32_t /* id */, const std::string& /*accountId*/, const std::string& /* conversationId */, std::vector<std::map<std::string, std::string>> /*messages*/){}
     virtual void messageReceived(const std::string& /*accountId*/, const std::string& /* conversationId */, std::map<std::string, std::string> /*message*/){}
+    virtual void conversationProfileUpdated(const std::string& /*accountId*/, const std::string& /* conversationId */, std::map<std::string, std::string> /*profile*/){}
     virtual void conversationRequestReceived(const std::string& /*accountId*/, const std::string& /* conversationId */, std::map<std::string, std::string> /*metadatas*/){}
     virtual void conversationRequestDeclined(const std::string& /*accountId*/, const std::string& /* conversationId */){}
     virtual void conversationReady(const std::string& /*accountId*/, const std::string& /* conversationId */){}
diff --git a/bin/jni/jni_interface.i b/bin/jni/jni_interface.i
index 89b05efa8251bf4a0001ea0095042a165331f7ce..80645527a225a48f3f524a017f2e6846d9c83bac 100644
--- a/bin/jni/jni_interface.i
+++ b/bin/jni/jni_interface.i
@@ -321,6 +321,7 @@ void init(ConfigurationCallback* confM, Callback* callM, PresenceCallback* presM
     const std::map<std::string, SharedCallback> conversationHandlers = {
         exportable_callback<ConversationSignal::ConversationLoaded>(bind(&ConversationCallback::conversationLoaded, convM, _1, _2, _3, _4)),
         exportable_callback<ConversationSignal::MessageReceived>(bind(&ConversationCallback::messageReceived, convM, _1, _2, _3)),
+        exportable_callback<ConversationSignal::ConversationProfileUpdated>(bind(&ConversationCallback::conversationProfileUpdated, convM, _1, _2, _3)),
         exportable_callback<ConversationSignal::ConversationRequestReceived>(bind(&ConversationCallback::conversationRequestReceived, convM, _1, _2, _3)),
         exportable_callback<ConversationSignal::ConversationRequestDeclined>(bind(&ConversationCallback::conversationRequestDeclined, convM, _1, _2)),
         exportable_callback<ConversationSignal::ConversationReady>(bind(&ConversationCallback::conversationReady, convM, _1, _2)),
diff --git a/bin/nodejs/callback.h b/bin/nodejs/callback.h
index 632d7603a4b77752b932bdbf7ad75252435e2519..f949639d8f9eb300a297a760c043f0ffd144c7fb 100644
--- a/bin/nodejs/callback.h
+++ b/bin/nodejs/callback.h
@@ -29,6 +29,7 @@ Persistent<Function> incomingCallCb;
 Persistent<Function> incomingCallWithMediaCb;
 Persistent<Function> conversationLoadedCb;
 Persistent<Function> messageReceivedCb;
+Persistent<Function> conversationProfileUpdatedCb;
 Persistent<Function> conversationRequestReceivedCb;
 Persistent<Function> conversationRequestDeclinedCb;
 Persistent<Function> conversationReadyCb;
@@ -88,6 +89,8 @@ getPresistentCb(std::string_view signal)
         return &conversationLoadedCb;
     else if (signal == "MessageReceived")
         return &messageReceivedCb;
+    else if (signal == "ConversationProfileUpdated")
+        return &conversationProfileUpdatedCb;
     else if (signal == "ConversationReady")
         return &conversationReadyCb;
     else if (signal == "ConversationRemoved")
@@ -450,12 +453,10 @@ callStateChanged(const std::string& accountId,
     pendingSignals.emplace([accountId, callId, state, detail_code]() {
         Local<Function> func = Local<Function>::New(Isolate::GetCurrent(), callStateChangedCb);
         if (!func.IsEmpty()) {
-            SWIGV8_VALUE callback_args[] = {
-                V8_STRING_NEW_LOCAL(accountId),
-                V8_STRING_NEW_LOCAL(callId),
-                V8_STRING_NEW_LOCAL(state),
-                SWIGV8_INTEGER_NEW(detail_code)
-            };
+            SWIGV8_VALUE callback_args[] = {V8_STRING_NEW_LOCAL(accountId),
+                                            V8_STRING_NEW_LOCAL(callId),
+                                            V8_STRING_NEW_LOCAL(state),
+                                            SWIGV8_INTEGER_NEW(detail_code)};
             func->Call(SWIGV8_CURRENT_CONTEXT(), SWIGV8_NULL(), 4, callback_args);
         }
     });
@@ -472,11 +473,9 @@ mediaChangeRequested(const std::string& accountId,
     pendingSignals.emplace([accountId, callId, mediaList]() {
         Local<Function> func = Local<Function>::New(Isolate::GetCurrent(), mediaChangeRequestedCb);
         if (!func.IsEmpty()) {
-            SWIGV8_VALUE callback_args[] = {
-                V8_STRING_NEW_LOCAL(accountId),
-                V8_STRING_NEW_LOCAL(callId),
-                stringMapVecToJsMapArray(mediaList)
-            };
+            SWIGV8_VALUE callback_args[] = {V8_STRING_NEW_LOCAL(accountId),
+                                            V8_STRING_NEW_LOCAL(callId),
+                                            stringMapVecToJsMapArray(mediaList)};
             func->Call(SWIGV8_CURRENT_CONTEXT(), SWIGV8_NULL(), 3, callback_args);
         }
     });
diff --git a/bin/nodejs/conversation.i b/bin/nodejs/conversation.i
index 9fd1319438e8eab169cfe0486a2cb3292a023ca4..dbc6107758b90b2a07dbd5d0acea8ce1129f82bd 100644
--- a/bin/nodejs/conversation.i
+++ b/bin/nodejs/conversation.i
@@ -27,6 +27,7 @@ public:
     virtual ~ConversationCallback(){}
     virtual void conversationLoaded(uint32_t /* id */, const std::string& /*accountId*/, const std::string& /* conversationId */, std::vector<std::map<std::string, std::string>> /*messages*/){}
     virtual void messageReceived(const std::string& /*accountId*/, const std::string& /* conversationId */, std::map<std::string, std::string> /*message*/){}
+    virtual void conversationProfileUpdated(const std::string& /*accountId*/, const std::string& /* conversationId */, std::map<std::string, std::string> /*profile*/){}
     virtual void conversationRequestReceived(const std::string& /*accountId*/, const std::string& /* conversationId */, std::map<std::string, std::string> /*metadatas*/){}
     virtual void conversationRequestDeclined(const std::string& /*accountId*/, const std::string& /* conversationId */){}
     virtual void conversationReady(const std::string& /*accountId*/, const std::string& /* conversationId */){}
@@ -67,6 +68,7 @@ public:
     virtual ~ConversationCallback(){}
     virtual void conversationLoaded(uint32_t /* id */, const std::string& /*accountId*/, const std::string& /* conversationId */, std::vector<std::map<std::string, std::string>> /*messages*/){}
     virtual void messageReceived(const std::string& /*accountId*/, const std::string& /* conversationId */, std::map<std::string, std::string> /*message*/){}
+    virtual void conversationProfileUpdated(const std::string& /*accountId*/, const std::string& /* conversationId */, std::map<std::string, std::string> /*profile*/){}
     virtual void conversationRequestReceived(const std::string& /*accountId*/, const std::string& /* conversationId */, std::map<std::string, std::string> /*metadatas*/){}
     virtual void conversationRequestDeclined(const std::string& /*accountId*/, const std::string& /* conversationId */){}
     virtual void conversationReady(const std::string& /*accountId*/, const std::string& /* conversationId */){}
diff --git a/bin/nodejs/nodejs_interface.i b/bin/nodejs/nodejs_interface.i
index 0bc32bba1b4f1492f37e7d041979e3da8597cfec..7084f08444be4770d65935cadbdfa2dfac8b902b 100644
--- a/bin/nodejs/nodejs_interface.i
+++ b/bin/nodejs/nodejs_interface.i
@@ -147,6 +147,7 @@ void init(const SWIGV8_VALUE& funcMap){
     const std::map<std::string, SharedCallback> conversationHandlers = {
         exportable_callback<ConversationSignal::ConversationLoaded>(bind(&conversationLoaded, _1, _2, _3, _4)),
         exportable_callback<ConversationSignal::MessageReceived>(bind(&messageReceived, _1, _2, _3)),
+        exportable_callback<ConversationSignal::ConversationProfileUpdated>(bind(&conversationProfileUpdated, _1, _2, _3)),
         exportable_callback<ConversationSignal::ConversationRequestReceived>(bind(&conversationRequestReceived, _1, _2, _3)),
         exportable_callback<ConversationSignal::ConversationRequestDeclined>(bind(&conversationRequestDeclined, _1, _2)),
         exportable_callback<ConversationSignal::ConversationReady>(bind(&conversationReady, _1, _2)),
diff --git a/src/client/ring_signal.cpp b/src/client/ring_signal.cpp
index 3b3d802ed0cee3ff0c2e78a7403cf8085c56b9c6..b31cc746f432fa2d3bd07be7fc76b762ec6e4e17 100644
--- a/src/client/ring_signal.cpp
+++ b/src/client/ring_signal.cpp
@@ -128,6 +128,7 @@ getSignalHandlers()
         /* Conversation */
         exported_callback<DRing::ConversationSignal::ConversationLoaded>(),
         exported_callback<DRing::ConversationSignal::MessageReceived>(),
+        exported_callback<DRing::ConversationSignal::ConversationProfileUpdated>(),
         exported_callback<DRing::ConversationSignal::ConversationRequestReceived>(),
         exported_callback<DRing::ConversationSignal::ConversationRequestDeclined>(),
         exported_callback<DRing::ConversationSignal::ConversationReady>(),
diff --git a/src/jami/conversation_interface.h b/src/jami/conversation_interface.h
index 69a8ea5b7fec92868ed985991fa0b47942dc2bb2..06dcaf209dd47d5591f903f7f240690de0176252 100644
--- a/src/jami/conversation_interface.h
+++ b/src/jami/conversation_interface.h
@@ -92,6 +92,13 @@ struct DRING_PUBLIC ConversationSignal
                              const std::string& /* conversationId */,
                              std::map<std::string, std::string> /*message*/);
     };
+    struct DRING_PUBLIC ConversationProfileUpdated
+    {
+        constexpr static const char* name = "ConversationProfileUpdated";
+        using cb_type = void(const std::string& /*accountId*/,
+                             const std::string& /* conversationId */,
+                             std::map<std::string, std::string> /*profile*/);
+    };
     struct DRING_PUBLIC ConversationRequestReceived
     {
         constexpr static const char* name = "ConversationRequestReceived";
diff --git a/src/jamidht/conversation.cpp b/src/jamidht/conversation.cpp
index 10a19a554b601f874ddcac1c8098b7dd454f3c2f..fa4dd138258e80c9d544c84f50b389adc0758c2d 100644
--- a/src/jamidht/conversation.cpp
+++ b/src/jamidht/conversation.cpp
@@ -770,7 +770,7 @@ Conversation::sendMessage(std::string&& message,
 }
 
 void
-Conversation::sendMessage(Json::Value&& value, const std::string& /*parent*/, OnDoneCb&& cb)
+Conversation::sendMessage(Json::Value&& value, const std::string& parent, OnDoneCb&& cb)
 {
     dht::ThreadPool::io().run([w = weak(), value = std::move(value), cb = std::move(cb)] {
         if (auto sthis = w.lock()) {
@@ -934,6 +934,7 @@ Conversation::pull(const std::string& deviceId, OnPullCb&& cb, std::string commi
         auto sthis_ = w.lock();
         if (!sthis_)
             return;
+        auto& repo = sthis_->pimpl_->repository_;
 
         std::string deviceId, commitId;
         OnPullCb cb;
@@ -973,8 +974,7 @@ Conversation::pull(const std::string& deviceId, OnPullCb&& cb, std::string commi
                 it = itr.first;
             }
             // If recently fetched, the commit can already be there, so no need to do complex operations
-            if (commitId != ""
-                && sthis_->pimpl_->repository_->getCommit(commitId, false) != std::nullopt) {
+            if (commitId != "" && repo->getCommit(commitId, false) != std::nullopt) {
                 cb(true);
                 std::lock_guard<std::mutex> lk(sthis_->pimpl_->pullcbsMtx_);
                 sthis_->pimpl_->fetchingRemotes_.erase(it);
@@ -991,12 +991,25 @@ Conversation::pull(const std::string& deviceId, OnPullCb&& cb, std::string commi
                 cb(false);
                 continue;
             }
+            auto oldId = repo->getHead();
             std::unique_lock<std::mutex> lk(sthis_->pimpl_->writeMtx_);
             auto newCommits = sthis_->mergeHistory(deviceId);
             sthis_->pimpl_->announce(newCommits);
             lk.unlock();
             if (cb)
                 cb(true);
+            // Announce if profile changed
+            auto newId = repo->getHead();
+            if (oldId != newId) {
+                auto diffStats = repo->diffStats(newId, oldId);
+                auto changedFiles = repo->changedFiles(diffStats);
+                if (find(changedFiles.begin(), changedFiles.end(), "profile.vcf")
+                    != changedFiles.end()) {
+                    if (auto account = sthis_->pimpl_->account_.lock())
+                        emitSignal<DRing::ConversationSignal::ConversationProfileUpdated>(
+                            account->getAccountID(), repo->id(), repo->infos());
+                }
+            }
         }
     });
 }
@@ -1095,12 +1108,16 @@ Conversation::updateInfos(const std::map<std::string, std::string>& map, const O
 {
     dht::ThreadPool::io().run([w = weak(), map = std::move(map), cb = std::move(cb)] {
         if (auto sthis = w.lock()) {
+            auto& repo = sthis->pimpl_->repository_;
             std::unique_lock<std::mutex> lk(sthis->pimpl_->writeMtx_);
-            auto commit = sthis->pimpl_->repository_->updateInfos(map);
+            auto commit = repo->updateInfos(map);
             sthis->pimpl_->announce(commit);
             lk.unlock();
             if (cb)
                 cb(!commit.empty(), commit);
+            if (auto account = sthis->pimpl_->account_.lock())
+                emitSignal<DRing::ConversationSignal::ConversationProfileUpdated>(
+                    account->getAccountID(), repo->id(), repo->infos());
         }
     });
 }
diff --git a/src/jamidht/conversationrepository.cpp b/src/jamidht/conversationrepository.cpp
index fd6df772853a277a4de08878faabdb7c0d96b995..edd4adc03bcf67c39aeaeb1c8661f46c2d99314f 100644
--- a/src/jamidht/conversationrepository.cpp
+++ b/src/jamidht/conversationrepository.cpp
@@ -3573,4 +3573,19 @@ ConversationRepository::uriFromDevice(const std::string& deviceId) const
     return pimpl_->uriFromDevice(deviceId);
 }
 
+std::string
+ConversationRepository::getHead() const
+{
+    if (auto repo = pimpl_->repository()) {
+        git_oid commit_id;
+        if (git_reference_name_to_id(&commit_id, repo.get(), "HEAD") < 0) {
+            JAMI_ERR("Cannot get reference for HEAD");
+            return {};
+        }
+        if (auto commit_str = git_oid_tostr_s(&commit_id))
+            return commit_str;
+    }
+    return {};
+}
+
 } // namespace jami
diff --git a/src/jamidht/conversationrepository.h b/src/jamidht/conversationrepository.h
index 89b8c6d2c4a0dbcabec1e5920ed037e210c73177..f085287ee2248fd9303440602ce0f3e8adb86479 100644
--- a/src/jamidht/conversationrepository.h
+++ b/src/jamidht/conversationrepository.h
@@ -359,6 +359,11 @@ public:
      */
     std::string uriFromDevice(const std::string& deviceId) const;
 
+    /**
+     * Get current HEAD hash
+     */
+    std::string getHead() const;
+
 private:
     ConversationRepository() = delete;
     class Impl;
diff --git a/test/unitTest/conversation/conversation.cpp b/test/unitTest/conversation/conversation.cpp
index 2d5925898839533ca7db5769a74127657e37cdad..a309c29cdd16bc2305c397c63b7ece7fd90c00fb 100644
--- a/test/unitTest/conversation/conversation.cpp
+++ b/test/unitTest/conversation/conversation.cpp
@@ -1920,8 +1920,6 @@ ConversationTest::testUpdateProfile()
     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 */,
@@ -1933,6 +1931,7 @@ ConversationTest::testUpdateProfile()
             }
             cv.notify_one();
         }));
+    bool requestReceived = false;
     confHandlers.insert(
         DRing::exportable_callback<DRing::ConversationSignal::ConversationRequestReceived>(
             [&](const std::string& /*accountId*/,
@@ -1941,6 +1940,7 @@ ConversationTest::testUpdateProfile()
                 requestReceived = true;
                 cv.notify_one();
             }));
+    bool conversationReady = false;
     confHandlers.insert(DRing::exportable_callback<DRing::ConversationSignal::ConversationReady>(
         [&](const std::string& accountId, const std::string& /* conversationId */) {
             if (accountId == bobId) {
@@ -1948,6 +1948,17 @@ ConversationTest::testUpdateProfile()
                 cv.notify_one();
             }
         }));
+    std::map<std::string, std::string> profileAlice, profileBob;
+    confHandlers.insert(
+        DRing::exportable_callback<DRing::ConversationSignal::ConversationProfileUpdated>(
+            [&](const auto& accountId, const auto& /* conversationId */, const auto& profile) {
+                if (accountId == aliceId) {
+                    profileAlice = profile;
+                } else if (accountId == bobId) {
+                    profileBob = profile;
+                }
+                cv.notify_one();
+            }));
     DRing::registerSignalHandlers(confHandlers);
 
     auto convId = DRing::startConversation(aliceId);
@@ -1962,11 +1973,18 @@ ConversationTest::testUpdateProfile()
 
     messageBobReceived = 0;
     aliceAccount->convModule()->updateConversationInfos(convId, {{"title", "My awesome swarm"}});
-    CPPUNIT_ASSERT(cv.wait_for(lk, 30s, [&]() { return messageBobReceived == 1; }));
+    CPPUNIT_ASSERT(cv.wait_for(lk, 30s, [&]() {
+        return messageBobReceived == 1 && !profileAlice.empty() && !profileBob.empty();
+    }));
 
     auto infos = DRing::conversationInfos(bobId, convId);
+    // Verify that we have the same profile everywhere
     CPPUNIT_ASSERT(infos["title"] == "My awesome swarm");
+    CPPUNIT_ASSERT(profileAlice["title"] == "My awesome swarm");
+    CPPUNIT_ASSERT(profileBob["title"] == "My awesome swarm");
     CPPUNIT_ASSERT(infos["description"].empty());
+    CPPUNIT_ASSERT(profileAlice["description"].empty());
+    CPPUNIT_ASSERT(profileBob["description"].empty());
 
     DRing::unregisterSignalHandlers();
 }