diff --git a/libpurple/purpleconversation.h b/libpurple/purpleconversation.h
--- a/libpurple/purpleconversation.h
+++ b/libpurple/purpleconversation.h
@@ -151,6 +151,18 @@
 PurpleConversationUiOps *purple_conversation_get_ui_ops(PurpleConversation *conv);
 
 /**
+ * purple_conversation_get_id:
+ * @conversation: The instance.
+ *
+ * Gets the opaque identifier from @conversation.
+ *
+ * Returns: (nullable): The id of @conversation.
+ *
+ * Since: 3.0.0
+ */
+const char *purple_conversation_get_id(PurpleConversation *conversation);
+
+/**
  * purple_conversation_set_account:
  * @conv: The conversation.
  * @account: The purple_account.
# HG changeset patch
# Node ID d9b9d624ee73
# Parent  790189f5b6de
diff --git a/libpurple/purpleconversation.c b/libpurple/purpleconversation.c
--- a/libpurple/purpleconversation.c
+++ b/libpurple/purpleconversation.c
@@ -31,6 +31,7 @@
 #include "server.h"
 
 typedef struct {
+	char *id;
 	PurpleAccount *account;
 
 	char *name;
@@ -45,6 +46,7 @@
 
 enum {
 	PROP_0,
+	PROP_ID,
 	PROP_ACCOUNT,
 	PROP_NAME,
 	PROP_TITLE,
@@ -67,6 +69,22 @@
 /**************************************************************************
  * Helpers
  **************************************************************************/
+static void
+purple_conversation_set_id(PurpleConversation *conversation, const char *id) {
+	PurpleConversationPrivate *priv = NULL;
+
+	g_return_if_fail(PURPLE_IS_CONVERSATION(conversation));
+
+	priv = purple_conversation_get_instance_private(conversation);
+
+	if(!purple_strequal(id, priv->id)) {
+		g_free(priv->id);
+		priv->id = g_strdup(id);
+
+		g_object_notify_by_pspec(G_OBJECT(conversation), properties[PROP_ID]);
+	}
+}
+
 static gboolean
 purple_conversation_check_member_equal(gconstpointer a, gconstpointer b) {
 	PurpleConversationMember *member_a = (PurpleConversationMember *)a;
@@ -232,6 +250,9 @@
 	priv = purple_conversation_get_instance_private(conv);
 
 	switch (param_id) {
+		case PROP_ID:
+			purple_conversation_set_id(conv, g_value_get_string(value));
+			break;
 		case PROP_ACCOUNT:
 			purple_conversation_set_account(conv, g_value_get_object(value));
 			break;
@@ -259,6 +280,9 @@
 	PurpleConversation *conv = PURPLE_CONVERSATION(obj);
 
 	switch(param_id) {
+		case PROP_ID:
+			g_value_set_string(value, purple_conversation_get_id(conv));
+			break;
 		case PROP_ACCOUNT:
 			g_value_set_object(value, purple_conversation_get_account(conv));
 			break;
@@ -359,6 +383,7 @@
 		ops->destroy_conversation(conv);
 	}
 
+	g_clear_pointer(&priv->id, g_free);
 	g_clear_pointer(&priv->name, g_free);
 	g_clear_pointer(&priv->title, g_free);
 	g_clear_object(&priv->members);
@@ -376,6 +401,20 @@
 	obj_class->get_property = purple_conversation_get_property;
 	obj_class->set_property = purple_conversation_set_property;
 
+	/**
+	 * PurpleConversation::id:
+	 *
+	 * An opaque identifier for this conversation. Generally speaking this is
+	 * protocol dependent and should only be used as a unique identifier.
+	 *
+	 * Since: 3.0.0
+	 */
+	properties[PROP_ID] = g_param_spec_string(
+		"id", "id",
+		"The identifier for the conversation.",
+		NULL,
+		G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS);
+
 	properties[PROP_ACCOUNT] = g_param_spec_object(
 		"account", "Account",
 		"The account for the conversation.",
@@ -541,6 +580,17 @@
 	return priv->ui_ops;
 }
 
+const char *
+purple_conversation_get_id(PurpleConversation *conversation) {
+	PurpleConversationPrivate *priv = NULL;
+
+	g_return_val_if_fail(PURPLE_IS_CONVERSATION(conversation), NULL);
+
+	priv = purple_conversation_get_instance_private(conversation);
+
+	return priv->id;
+}
+
 void
 purple_conversation_set_account(PurpleConversation *conv,
                                 PurpleAccount *account)
diff --git a/libpurple/purpleconversationmanager.h b/libpurple/purpleconversationmanager.h
--- a/libpurple/purpleconversationmanager.h
+++ b/libpurple/purpleconversationmanager.h
@@ -195,6 +195,21 @@
  */
 PurpleConversation *purple_conversation_manager_find_chat_by_id(PurpleConversationManager *manager, PurpleAccount *account, gint id);
 
+/**
+ * purple_conversation_manager_find_with_id:
+ * @manager: The instance.
+ * @account: The account the conversation belongs to.
+ * @id: The identifier of the conversation.
+ *
+ * Looks for a registered conversation belonging to @account with an id of @id.
+ *
+ * Returns: (transfer none) (nullable): The [class@PurpleConversation] if
+ *          found, otherwise %NULL.
+ *
+ * Since: 3.0.0
+ */
+PurpleConversation *purple_conversation_manager_find_with_id(PurpleConversationManager *manager, PurpleAccount *account, const char *id);
+
 G_END_DECLS
 
 #endif /* PURPLE_CONVERSATION_MANAGER_H */
diff --git a/libpurple/purpleconversationmanager.c b/libpurple/purpleconversationmanager.c
--- a/libpurple/purpleconversationmanager.c
+++ b/libpurple/purpleconversationmanager.c
@@ -76,6 +76,20 @@
 	return (purple_chat_conversation_get_id(chat) == id);
 }
 
+static gboolean
+purple_conversation_has_id(PurpleConversation *conversation, gpointer data) {
+	const char *needle = data;
+	const char *haystack = NULL;
+
+	if(!PURPLE_IS_CONVERSATION(conversation)) {
+		return FALSE;
+	}
+
+	haystack = purple_conversation_get_id(conversation);
+
+	return purple_strequal(needle, haystack);
+}
+
 static PurpleConversation *
 purple_conversation_manager_find_internal(PurpleConversationManager *manager,
                                           PurpleAccount *account,
@@ -330,3 +344,16 @@
 	                                                 purple_conversation_chat_has_id,
 	                                                 GINT_TO_POINTER(id));
 }
+
+PurpleConversation *
+purple_conversation_manager_find_with_id(PurpleConversationManager *manager,
+                                         PurpleAccount *account,
+                                         const char *id)
+{
+	g_return_val_if_fail(PURPLE_IS_CONVERSATION_MANAGER(manager), NULL);
+	g_return_val_if_fail(PURPLE_IS_ACCOUNT(account), NULL);
+
+	return purple_conversation_manager_find_internal(manager, account, NULL,
+	                                                 purple_conversation_has_id,
+	                                                 (gpointer)id);
+}
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
@@ -33,8 +33,9 @@
 	PurpleConversation *conversation = NULL;
 	PurpleConversationManager *conversation_manager = NULL;
 	GListModel *members = NULL;
-	gchar *name = NULL;
-	gchar *title = NULL;
+	char *id = NULL;
+	char *name = NULL;
+	char *title = NULL;
 
 	account = purple_account_new("test", "test");
 
@@ -44,6 +45,7 @@
 	 */
 	conversation = g_object_new(
 		PURPLE_TYPE_CONVERSATION,
+		"id", "id1",
 		"account", account,
 		"features", PURPLE_CONNECTION_FLAG_HTML,
 		"name", "name1",
@@ -52,6 +54,7 @@
 
 	/* Now use g_object_get to read all of the properties. */
 	g_object_get(conversation,
+		"id", &id,
 		"account", &account1,
 		"features", &features,
 		"members", &members,
@@ -60,6 +63,7 @@
 		NULL);
 
 	/* Compare all the things. */
+	g_assert_cmpstr(id, ==, "id1");
 	g_assert_true(account1 == account);
 	g_assert_cmpint(features, ==, PURPLE_CONNECTION_FLAG_HTML);
 	g_assert_true(G_IS_LIST_MODEL(members));
@@ -79,10 +83,11 @@
 	purple_conversation_manager_unregister(conversation_manager, conversation);
 
 	/* Free/unref all the things. */
+	g_clear_pointer(&id, g_free);
 	g_clear_object(&account1);
 	g_clear_object(&members);
-	g_free(name);
-	g_free(title);
+	g_clear_pointer(&name, g_free);
+	g_clear_pointer(&title, g_free);
 
 	g_clear_object(&account);
 	g_clear_object(&conversation);
