diff --git a/libpurple/purpleconversation.h b/libpurple/purpleconversation.h
--- a/libpurple/purpleconversation.h
+++ b/libpurple/purpleconversation.h
@@ -182,6 +182,10 @@
  *
  * Gets the opaque identifier from @conversation.
  *
+ * This value is specific to the protocol and could conflict with the same
+ * conversation on other accounts. To get a libpurple unique id see
+ * [method@Conversation.get_global_id].
+ *
  * Returns: (nullable): The id of @conversation.
  *
  * Since: 3.0
@@ -190,6 +194,22 @@
 const char *purple_conversation_get_id(PurpleConversation *conversation);
 
 /**
+ * purple_conversation_get_global_id:
+ * @conversation: The instance.
+ *
+ * Gets a globally unique id for @conversation.
+ *
+ * This id ties the [property@Conversation:id] to the [class@Account] that the
+ * conversation is for.
+ *
+ * Returns: (transfer full): A globally unique id of @conversation.
+ *
+ * Since: 3.0
+ */
+PURPLE_AVAILABLE_IN_3_0
+char *purple_conversation_get_global_id(PurpleConversation *conversation);
+
+/**
  * purple_conversation_get_conversation_type:
  * @conversation: The instance.
  *
# HG changeset patch
# Node ID 411fe2b2837db63f06f98982c1a98e2ac5b0c54b
# Parent  8b06291db25c24ae815aa9fdd6163310048389fe
diff --git a/libpurple/purpleconversation.c b/libpurple/purpleconversation.c
--- a/libpurple/purpleconversation.c
+++ b/libpurple/purpleconversation.c
@@ -72,6 +72,7 @@
 enum {
 	PROP_0,
 	PROP_ID,
+	PROP_GLOBAL_ID,
 	PROP_TYPE,
 	PROP_ACCOUNT,
 	PROP_AVATAR,
@@ -156,7 +157,12 @@
 	g_return_if_fail(PURPLE_IS_CONVERSATION(conversation));
 
 	if(g_set_str(&conversation->id, id)) {
-		g_object_notify_by_pspec(G_OBJECT(conversation), properties[PROP_ID]);
+		GObject *obj = G_OBJECT(conversation);
+
+		g_object_freeze_notify(obj);
+		g_object_notify_by_pspec(obj, properties[PROP_ID]);
+		g_object_notify_by_pspec(obj, properties[PROP_GLOBAL_ID]);
+		g_object_thaw_notify(obj);
 	}
 }
 
@@ -178,6 +184,8 @@
 	}
 
 	if(g_set_object(&conversation->account, account)) {
+		GObject *obj = NULL;
+
 		if(PURPLE_IS_ACCOUNT(conversation->account)) {
 			purple_conversation_add_member(conversation,
 			                               PURPLE_CONTACT_INFO(account),
@@ -188,8 +196,11 @@
 			                        conversation, 0);
 		}
 
-		g_object_notify_by_pspec(G_OBJECT(conversation),
-		                         properties[PROP_ACCOUNT]);
+		obj = G_OBJECT(conversation);
+		g_object_freeze_notify(obj);
+		g_object_notify_by_pspec(obj, properties[PROP_ACCOUNT]);
+		g_object_notify_by_pspec(obj, properties[PROP_GLOBAL_ID]);
+		g_object_thaw_notify(obj);
 	}
 }
 
@@ -533,6 +544,10 @@
 	case PROP_ID:
 		g_value_set_string(value, purple_conversation_get_id(conversation));
 		break;
+	case PROP_GLOBAL_ID:
+		g_value_take_string(value,
+		                    purple_conversation_get_global_id(conversation));
+		break;
 	case PROP_TYPE:
 		g_value_set_enum(value,
 		                 purple_conversation_get_conversation_type(conversation));
@@ -723,6 +738,22 @@
 		G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS);
 
 	/**
+	 * PurpleConversation:global-id:
+	 *
+	 * A libpurple global id for the conversation.
+	 *
+	 * This is an opaque value but it ties the conversation to the libpurple
+	 * account it belongs to, which makes it globally unique inside of
+	 * libpurple.
+	 *
+	 * Since: 3.0
+	 */
+	properties[PROP_GLOBAL_ID] = g_param_spec_string(
+		"global-id", NULL, NULL,
+		NULL,
+		G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);
+
+	/**
 	 * PurpleConversation:type:
 	 *
 	 * A type hint for the conversation. This may be useful for protocols, but
@@ -1220,6 +1251,17 @@
 	return conversation->id;
 }
 
+char *
+purple_conversation_get_global_id(PurpleConversation *conversation) {
+	const char *account_id = NULL;
+
+	g_return_val_if_fail(PURPLE_IS_CONVERSATION(conversation), NULL);
+
+	account_id = purple_account_get_id(conversation->account);
+
+	return g_strdup_printf("%s-%s", account_id, conversation->id);
+}
+
 PurpleConversationType
 purple_conversation_get_conversation_type(PurpleConversation *conversation) {
 	g_return_val_if_fail(PURPLE_IS_CONVERSATION(conversation),
diff --git a/libpurple/tests/test_conversation.c b/libpurple/tests/test_conversation.c
--- a/libpurple/tests/test_conversation.c
+++ b/libpurple/tests/test_conversation.c
@@ -61,6 +61,7 @@
 	GListModel *members = NULL;
 	char *alias = NULL;
 	char *description = NULL;
+	char *global_id = NULL;
 	char *id = NULL;
 	char *name = NULL;
 	char *title = NULL;
@@ -70,7 +71,12 @@
 	gboolean age_restricted = FALSE;
 	gboolean favorite = FALSE;
 
-	account = purple_account_new("test", "test");
+	account = g_object_new(
+		PURPLE_TYPE_ACCOUNT,
+		"id", "test",
+		"protocol-id", "test",
+		NULL);
+
 	avatar = g_object_new(PURPLE_TYPE_AVATAR, NULL);
 	creator = purple_contact_info_new(NULL);
 	created_on = g_date_time_new_now_utc();
@@ -113,6 +119,7 @@
 		"description", &description,
 		"favorite", &favorite,
 		"features", &features,
+		"global-id", &global_id,
 		"id", &id,
 		"members", &members,
 		"name", &name,
@@ -152,6 +159,9 @@
 
 	g_assert_cmpint(features, ==, PURPLE_CONNECTION_FLAG_HTML);
 
+	g_assert_cmpstr(global_id, ==, "test-id1");
+	g_clear_pointer(&global_id, g_free);
+
 	g_assert_cmpstr(id, ==, "id1");
 	g_clear_pointer(&id, g_free);
 
