diff --git a/libpurple/purpleprotocolconversation.h b/libpurple/purpleprotocolconversation.h
--- a/libpurple/purpleprotocolconversation.h
+++ b/libpurple/purpleprotocolconversation.h
@@ -34,6 +34,7 @@
 #include "purpleavatar.h"
 #include "purplechanneljoindetails.h"
 #include "purpleconversation.h"
+#include "purplecreateconversationdetails.h"
 #include "purplemessage.h"
 #include "purpleprotocol.h"
 #include "purpletyping.h"
@@ -41,6 +42,17 @@
 
 G_BEGIN_DECLS
 
+/**
+ * PURPLE_PROTOCOL_CONVERSATION_DOMAIN:
+ *
+ * An error domain for [iface@ProtocolConversation] errors.
+ *
+ * Since: 3.0
+ */
+#define PURPLE_PROTOCOL_CONVERSATION_DOMAIN \
+	(g_quark_from_static_string("purple-protocol-conversation")) \
+	PURPLE_AVAILABLE_MACRO_IN_3_0
+
 #define PURPLE_TYPE_PROTOCOL_CONVERSATION (purple_protocol_conversation_get_type())
 
 PURPLE_AVAILABLE_IN_3_0
@@ -61,6 +73,10 @@
 	GTypeInterface parent;
 
 	/*< public >*/
+	PurpleCreateConversationDetails *(*get_create_conversation_details)(PurpleProtocolConversation *protocol, PurpleAccount *account);
+	void (*create_conversation_async)(PurpleProtocolConversation *protocol, PurpleAccount *account, PurpleCreateConversationDetails *details, GCancellable *cancellable, GAsyncReadyCallback callback, gpointer data);
+	PurpleConversation *(*create_conversation_finish)(PurpleProtocolConversation *protocol, GAsyncResult *result, GError **error);
+
 	void (*send_message_async)(PurpleProtocolConversation *protocol, PurpleConversation *conversation, PurpleMessage *message, GCancellable *cancellable, GAsyncReadyCallback callback, gpointer data);
 	gboolean (*send_message_finish)(PurpleProtocolConversation *protocol, GAsyncResult *result, GError **error);
 
@@ -81,6 +97,81 @@
 };
 
 /**
+ * purple_protocol_conversation_impelements_create:
+ * @protocol: The instance.
+ *
+ * Checks if @protocol implements [iface@ProtocolConversation] and implements
+ * [vfunc@ProtocolConversation.get_create_conversation_details],
+ * [vfunc@ProtocolConversation.create_conversation_async], and
+ * [vfunc@ProtocolConversation.create_conversation_finish].
+ *
+ * Returns: %TRUE if everything is implemented, otherwise %FALSE.
+ *
+ * Since: 3.0
+ */
+PURPLE_AVAILABLE_IN_3_0
+gboolean purple_protocol_conversation_implements_create(PurpleProtocol *protocol);
+
+/**
+ * purple_protocol_conversation_get_create_conversation_details:
+ * @protocol: The instance.
+ * @account: The account.
+ *
+ * Gets a [class@CreateConversationDetails] from @protocol for @account.
+ *
+ * This can then be used in calls to
+ * [method@ProtocolConversation.create_conversation_async] to create the
+ * conversation.
+ *
+ * Returns: (transfer full): The details to create a conversation with
+ *          @account.
+ *
+ * Since: 3.0
+ */
+PURPLE_AVAILABLE_IN_3_0
+PurpleCreateConversationDetails *purple_protocol_conversation_get_create_conversation_details(PurpleProtocolConversation *protocol, PurpleAccount *account);
+
+/**
+ * purple_protocol_conversation_create_conversation_async:
+ * @protocol: The instance.
+ * @account: The account.
+ * @details: (transfer full): The create conversation details.
+ * @cancellable: (nullable): optional GCancellable object, %NULL to ignore.
+ * @callback: (nullable) (scope async): The callback to call after the
+ *            conversation has been created.
+ * @data: (nullable): Optional user data to pass to @callback.
+ *
+ * Starts the process of creating a dm or group dm conversation on @account.
+ *
+ * Since: 3.0
+ */
+PURPLE_AVAILABLE_IN_3_0
+void purple_protocol_conversation_create_conversation_async(PurpleProtocolConversation *protocol, PurpleAccount *account, PurpleCreateConversationDetails *details, GCancellable *cancellable, GAsyncReadyCallback callback, gpointer data);
+
+/**
+ * purple_protocol_conversation_create_conversation_finish:
+ * @protocol: The instance.
+ * @result: The [iface@Gio.AsyncResult] from the previous
+ *          [method@ProtocolConversation.create_conversation_async] call.
+ * @error: Return address for a #GError, or %NULL.
+ *
+ * Finishes a previous call to
+ * [method@ProtocolConversation.create_conversation_async]. This should be
+ * called from the callback of that function to get the result of whether or
+ * not the conversation was created successfully.
+ *
+ * It is the responsibility of the protocol to register the conversation with
+ * the default [class@ConversationManager].
+ *
+ * Returns: (transfer full): The new conversation if successful, otherwise
+ *          %NULL with @error possibly set.
+ *
+ * Since: 3.0
+ */
+PURPLE_AVAILABLE_IN_3_0
+PurpleConversation *purple_protocol_conversation_create_conversation_finish(PurpleProtocolConversation *protocol, GAsyncResult *result, GError **error);
+
+/**
  * purple_protocol_conversation_send_message_async:
  * @protocol: The instance.
  * @conversation: The conversation to send a message to.
# HG changeset patch
# Node ID e69661465c08
# Parent  8019945e1c21
diff --git a/libpurple/purpleprotocolconversation.c b/libpurple/purpleprotocolconversation.c
--- a/libpurple/purpleprotocolconversation.c
+++ b/libpurple/purpleprotocolconversation.c
@@ -36,6 +36,117 @@
 /******************************************************************************
  * Public API
  *****************************************************************************/
+gboolean
+purple_protocol_conversation_implements_create(PurpleProtocol *protocol) {
+	PurpleProtocolConversation *protocol_conversation = NULL;
+	PurpleProtocolConversationInterface *iface = NULL;
+
+	g_return_val_if_fail(PURPLE_IS_PROTOCOL_CONVERSATION(protocol), FALSE);
+
+	protocol_conversation = PURPLE_PROTOCOL_CONVERSATION(protocol);
+	iface = PURPLE_PROTOCOL_CONVERSATION_GET_IFACE(protocol_conversation);
+
+	if(iface->get_create_conversation_details == NULL) {
+		return FALSE;
+	}
+
+	if(iface->create_conversation_async == NULL) {
+		return FALSE;
+	}
+
+	if(iface->create_conversation_finish == NULL) {
+		return FALSE;
+	}
+
+	return TRUE;
+}
+
+PurpleCreateConversationDetails *
+purple_protocol_conversation_get_create_conversation_details(PurpleProtocolConversation *protocol,
+                                                             PurpleAccount *account)
+{
+	PurpleProtocolConversationInterface *iface = NULL;
+
+	g_return_val_if_fail(PURPLE_IS_PROTOCOL_CONVERSATION(protocol), NULL);
+	g_return_val_if_fail(PURPLE_IS_ACCOUNT(account), NULL);
+
+	iface = PURPLE_PROTOCOL_CONVERSATION_GET_IFACE(protocol);
+	if(iface != NULL && iface->get_create_conversation_details != NULL) {
+		return iface->get_create_conversation_details(protocol, account);
+	}
+
+	g_warning("%s does not implement get_create_conversation_details",
+	          G_OBJECT_TYPE_NAME(protocol));
+
+	return NULL;
+}
+
+void
+purple_protocol_conversation_create_conversation_async(PurpleProtocolConversation *protocol,
+                                                       PurpleAccount *account,
+                                                       PurpleCreateConversationDetails *details,
+                                                       GCancellable *cancellable,
+                                                       GAsyncReadyCallback callback,
+                                                       gpointer data)
+{
+	PurpleProtocolConversationInterface *iface = NULL;
+
+	g_return_if_fail(PURPLE_IS_PROTOCOL_CONVERSATION(protocol));
+	g_return_if_fail(PURPLE_IS_ACCOUNT(account));
+	g_return_if_fail(PURPLE_IS_CREATE_CONVERSATION_DETAILS(details));
+
+	iface = PURPLE_PROTOCOL_CONVERSATION_GET_IFACE(protocol);
+	if(iface != NULL && iface->create_conversation_async != NULL) {
+		iface->create_conversation_async(protocol, account, details,
+		                                 cancellable, callback, data);
+	} else {
+		GTask *task = NULL;
+
+		task = g_task_new(protocol, cancellable, callback, data);
+		g_task_return_new_error(task, PURPLE_PROTOCOL_CONVERSATION_DOMAIN, 0,
+		                        "%s does not implement create_conversation_async",
+		                        G_OBJECT_TYPE_NAME(protocol));
+		g_task_set_source_tag(task,
+		                      purple_protocol_conversation_create_conversation_async);
+
+		/* details is transfer full and has already been null checked. */
+		g_object_unref(details);
+
+		g_clear_object(&task);
+	}
+}
+
+PurpleConversation *
+purple_protocol_conversation_create_conversation_finish(PurpleProtocolConversation *protocol,
+                                                        GAsyncResult *result,
+                                                        GError **error)
+{
+	PurpleProtocolConversationInterface *iface = NULL;
+
+	g_return_val_if_fail(PURPLE_IS_PROTOCOL_CONVERSATION(protocol), FALSE);
+	g_return_val_if_fail(G_IS_ASYNC_RESULT(result), FALSE);
+
+	iface = PURPLE_PROTOCOL_CONVERSATION_GET_IFACE(protocol);
+	if(iface != NULL && iface->create_conversation_finish != NULL) {
+		return iface->create_conversation_finish(protocol, result, error);
+	}
+
+	if(G_IS_TASK(result)) {
+		GTask *task = G_TASK(result);
+		gpointer source = NULL;
+
+		source = g_task_get_source_tag(task);
+		if(source == purple_protocol_conversation_create_conversation_async) {
+			return g_task_propagate_pointer(task, error);
+		} else {
+			g_warning("%s does not implement create_conversation_finish",
+			          G_OBJECT_TYPE_NAME(protocol));
+		}
+	}
+
+	return FALSE;
+}
+
 void
 purple_protocol_conversation_send_message_async(PurpleProtocolConversation *protocol,
                                                 PurpleConversation *conversation,
diff --git a/libpurple/tests/test_protocol_conversation.c b/libpurple/tests/test_protocol_conversation.c
--- a/libpurple/tests/test_protocol_conversation.c
+++ b/libpurple/tests/test_protocol_conversation.c
@@ -87,6 +87,97 @@
  * TestProtocolConversationEmpty Tests
  *****************************************************************************/
 static void
+test_purple_protocol_conversation_empty_get_create_conversation_details(void) {
+	if(g_test_subprocess()) {
+		PurpleAccount *account = NULL;
+		PurpleCreateConversationDetails *details = NULL;
+		PurpleProtocolConversation *protocol = NULL;
+
+		protocol = g_object_new(test_purple_protocol_conversation_empty_get_type(),
+		                        NULL);
+		account = purple_account_new("test", "test");
+		details = purple_protocol_conversation_get_create_conversation_details(protocol,
+		                                                                       account);
+
+		g_assert_null(details);
+		g_assert_finalize_object(account);
+		g_assert_finalize_object(protocol);
+	}
+
+	g_test_trap_subprocess(NULL, 0, 0);
+	g_test_trap_assert_stderr("*Purple-WARNING*TestPurpleProtocolConversationEmpty*create_conversation_details*");
+}
+
+static void
+test_purple_protocol_conversation_empty_create_conversation_async_cb(GObject *source,
+                                                                     GAsyncResult *result,
+                                                                     G_GNUC_UNUSED gpointer data)
+{
+	PurpleConversation *conversation = NULL;
+	GError *error = NULL;
+
+	g_assert_true(PURPLE_IS_PROTOCOL_CONVERSATION(source));
+
+	conversation = purple_protocol_conversation_create_conversation_finish(PURPLE_PROTOCOL_CONVERSATION(source),
+	                                                                       result,
+	                                                                       &error);
+	g_assert_error(error, PURPLE_PROTOCOL_CONVERSATION_DOMAIN, 0);
+	g_clear_error(&error);
+	g_assert_null(conversation);
+}
+
+static void
+test_purple_protocol_conversation_empty_create_conversation_async(void) {
+	PurpleAccount *account = NULL;
+	PurpleCreateConversationDetails *details = NULL;
+	PurpleProtocolConversation *protocol = NULL;
+
+	protocol = g_object_new(test_purple_protocol_conversation_empty_get_type(),
+	                        NULL);
+	account = purple_account_new("test", "test");
+	details = purple_create_conversation_details_new(0);
+
+	purple_protocol_conversation_create_conversation_async(protocol,
+	                                                       account,
+	                                                       details,
+	                                                       NULL,
+	                                                       test_purple_protocol_conversation_empty_create_conversation_async_cb,
+	                                                       NULL);
+
+	g_main_context_iteration(NULL, FALSE);
+
+	g_assert_finalize_object(account);
+	g_assert_finalize_object(protocol);
+}
+
+static void
+test_purple_protocol_conversation_empty_create_conversation_finish(void) {
+	if(g_test_subprocess()) {
+		PurpleProtocolConversation *protocol = NULL;
+		PurpleConversation *conversation = NULL;
+		GError *error = NULL;
+		GTask *task = NULL;
+
+		protocol = g_object_new(test_purple_protocol_conversation_empty_get_type(),
+		                        NULL);
+
+		task = g_task_new(protocol, NULL, NULL, NULL);
+
+		conversation = purple_protocol_conversation_create_conversation_finish(protocol,
+		                                                                       G_ASYNC_RESULT(task),
+		                                                                       &error);
+		g_assert_no_error(error);
+		g_assert_null(conversation);
+
+		g_assert_finalize_object(task);
+		g_assert_finalize_object(protocol);
+	}
+
+	g_test_trap_subprocess(NULL, 0, 0);
+	g_test_trap_assert_stderr("*Purple-WARNING*TestPurpleProtocolConversationEmpty*create_conversation_finish*");
+}
+
+static void
 test_purple_protocol_conversation_empty_send_message_async(void) {
 	if(g_test_subprocess()) {
 		PurpleAccount *account = NULL;
@@ -376,6 +467,10 @@
 
 	gboolean should_error;
 
+	guint get_create_conversation_details;
+	guint create_conversation_async;
+	guint create_conversation_finish;
+
 	guint send_message_async;
 	guint send_message_finish;
 
@@ -392,6 +487,73 @@
 	guint send_typing;
 };
 
+static PurpleCreateConversationDetails *
+test_purple_protocol_conversation_get_create_conversation_details(PurpleProtocolConversation *protocol,
+                                                                  PurpleAccount *account)
+{
+	TestPurpleProtocolConversation *test_protocol = NULL;
+
+	test_protocol = TEST_PURPLE_PROTOCOL_CONVERSATION(protocol);
+	test_protocol->get_create_conversation_details += 1;
+
+	g_assert_true(PURPLE_IS_PROTOCOL_CONVERSATION(protocol));
+	g_assert_true(PURPLE_IS_ACCOUNT(account));
+
+	return purple_create_conversation_details_new(10);
+}
+
+static void
+test_purple_protocol_conversation_create_conversation_async(PurpleProtocolConversation *protocol,
+                                                            PurpleAccount *account,
+                                                            PurpleCreateConversationDetails *details,
+                                                            GCancellable *cancellable,
+                                                            GAsyncReadyCallback callback,
+                                                            gpointer data)
+{
+	TestPurpleProtocolConversation *test_protocol = NULL;
+	GTask *task = NULL;
+
+	g_assert_true(PURPLE_IS_ACCOUNT(account));
+	g_assert_true(PURPLE_IS_CREATE_CONVERSATION_DETAILS(details));
+
+	/* details is transfer full and we're done with it. */
+	g_object_unref(details);
+
+	test_protocol = TEST_PURPLE_PROTOCOL_CONVERSATION(protocol);
+	test_protocol->create_conversation_async += 1;
+
+	task = g_task_new(protocol, cancellable, callback, data);
+	if(test_protocol->should_error) {
+		GError *error = g_error_new_literal(TEST_PURPLE_PROTOCOL_CONVERSATION_DOMAIN,
+		                                    0, "error");
+		g_task_return_error(task, error);
+	} else {
+		PurpleConversation *conversation = NULL;
+
+		conversation = g_object_new(
+			PURPLE_TYPE_CONVERSATION,
+			"account", account,
+			"name", "test",
+			NULL);
+		g_task_return_pointer(task, conversation, g_object_unref);
+	}
+
+	g_clear_object(&task);
+}
+
+static PurpleConversation *
+test_purple_protocol_conversation_create_conversation_finish(PurpleProtocolConversation *protocol,
+                                                             GAsyncResult *result,
+                                                             GError **error)
+{
+	TestPurpleProtocolConversation *test_protocol = NULL;
+
+	test_protocol = TEST_PURPLE_PROTOCOL_CONVERSATION(protocol);
+	test_protocol->create_conversation_finish += 1;
+
+	return g_task_propagate_pointer(G_TASK(result), error);
+}
+
 static void
 test_purple_protocol_conversation_send_message_async(PurpleProtocolConversation *protocol,
                                                      PurpleConversation *conversation,
@@ -590,6 +752,10 @@
 
 static void
 test_purple_protocol_conversation_iface_init(PurpleProtocolConversationInterface *iface) {
+	iface->get_create_conversation_details = test_purple_protocol_conversation_get_create_conversation_details;
+	iface->create_conversation_async = test_purple_protocol_conversation_create_conversation_async;
+	iface->create_conversation_finish = test_purple_protocol_conversation_create_conversation_finish;
+
 	iface->send_message_async = test_purple_protocol_conversation_send_message_async;
 	iface->send_message_finish = test_purple_protocol_conversation_send_message_finish;
 
@@ -615,6 +781,10 @@
 static void
 test_purple_protocol_conversation_init(TestPurpleProtocolConversation *protocol)
 {
+	protocol->get_create_conversation_details = 0;
+	protocol->create_conversation_async = 0;
+	protocol->create_conversation_finish = 0;
+
 	protocol->send_message_async = 0;
 	protocol->send_message_finish = 0;
 
@@ -637,6 +807,100 @@
 }
 
 /******************************************************************************
+ * TestProtocolConversation CreateConversation Tests
+ *****************************************************************************/
+static void
+test_purple_protocol_conversation_get_create_conversation_details_normal(void)
+{
+	TestPurpleProtocolConversation *test_protocol = NULL;
+	PurpleAccount *account = NULL;
+	PurpleCreateConversationDetails *details = NULL;
+	PurpleProtocolConversation *protocol = NULL;
+
+	test_protocol = g_object_new(test_purple_protocol_conversation_get_type(),
+	                             NULL);
+	protocol = PURPLE_PROTOCOL_CONVERSATION(test_protocol);
+
+	account = purple_account_new("test", "test");
+
+	details = purple_protocol_conversation_get_create_conversation_details(protocol,
+	                                                                       account);
+
+	g_assert_true(PURPLE_IS_CREATE_CONVERSATION_DETAILS(details));
+
+	g_assert_cmpuint(test_protocol->get_create_conversation_details, ==, 1);
+
+	g_assert_finalize_object(details);
+	g_assert_finalize_object(account);
+	g_assert_finalize_object(test_protocol);
+}
+
+static void
+test_purple_protocol_conversation_create_conversation_cb(GObject *obj,
+                                                         GAsyncResult *result,
+                                                         G_GNUC_UNUSED gpointer data)
+{
+	TestPurpleProtocolConversation *test_protocol = NULL;
+	PurpleConversation *conversation = NULL;
+	PurpleProtocolConversation *protocol = NULL;
+	GError *error = NULL;
+
+	protocol = PURPLE_PROTOCOL_CONVERSATION(obj);
+	test_protocol = TEST_PURPLE_PROTOCOL_CONVERSATION(obj);
+
+	conversation = purple_protocol_conversation_create_conversation_finish(protocol,
+	                                                                       result,
+	                                                                       &error);
+
+	if(test_protocol->should_error) {
+		g_assert_error(error, TEST_PURPLE_PROTOCOL_CONVERSATION_DOMAIN, 0);
+		g_clear_error(&error);
+		g_assert_null(conversation);
+	} else {
+		g_assert_no_error(error);
+		g_assert_true(PURPLE_IS_CONVERSATION(conversation));
+		g_clear_object(&conversation);
+	}
+}
+
+static void
+test_purple_protocol_conversation_create_conversation_normal(gconstpointer data)
+{
+	TestPurpleProtocolConversation *protocol = NULL;
+	PurpleAccount *account = NULL;
+	PurpleCreateConversationDetails *details = NULL;
+
+	protocol = g_object_new(test_purple_protocol_conversation_get_type(),
+	                        NULL);
+	protocol->should_error = GPOINTER_TO_INT(data);
+
+	account = purple_account_new("test", "test");
+	details = purple_create_conversation_details_new(10);
+
+	purple_protocol_conversation_create_conversation_async(PURPLE_PROTOCOL_CONVERSATION(protocol),
+	                                                       account,
+	                                                       details,
+	                                                       NULL,
+	                                                       test_purple_protocol_conversation_create_conversation_cb,
+	                                                       NULL);
+
+	while(g_main_context_iteration(NULL, FALSE));
+
+	g_assert_cmpuint(protocol->create_conversation_async, ==, 1);
+	g_assert_cmpuint(protocol->create_conversation_finish, ==, 1);
+
+	g_assert_finalize_object(protocol);
+
+	/* We can't use g_assert_finalize_object as current, PurpleAccount's
+	 * presence is a PurpleAccountPresence that holds a reference to the
+	 * account and we have no way to break that cycle. Trying to use dispose
+	 * doesn't work because the account and the presence can't even reach a
+	 * reference count of zero.
+	 */
+	g_clear_object(&account);
+}
+
+/******************************************************************************
  * TestProtocolConversation SendMessage Tests
  *****************************************************************************/
 static void
@@ -1017,6 +1281,14 @@
 
 	loop = g_main_loop_new(NULL, FALSE);
 
+	/* Empty create conversation tests. */
+	g_test_add_func("/protocol-conversation/empty/get-create-conversation-details",
+	                test_purple_protocol_conversation_empty_get_create_conversation_details);
+	g_test_add_func("/protocol-conversation/empty/create-conversation-async",
+	                test_purple_protocol_conversation_empty_create_conversation_async);
+	g_test_add_func("/protocol-conversation/empty/create-conversation-finish",
+	                test_purple_protocol_conversation_empty_create_conversation_finish);
+
 	/* Empty send message tests. */
 	g_test_add_func("/protocol-conversation/empty/send-message-async",
 	                test_purple_protocol_conversation_empty_send_message_async);
@@ -1043,6 +1315,16 @@
 	g_test_add_func("/protocol-conversation/empty/send-typing",
 	                test_purple_protocol_conversation_empty_send_typing);
 
+	/* Normal create conversation tests. */
+	g_test_add_func("/protocol-conversation/normal/get-create-conversation-details-normal",
+	                test_purple_protocol_conversation_get_create_conversation_details_normal);
+	g_test_add_data_func("/protocol-conversation/normal/create-conversation-normal",
+	                     GINT_TO_POINTER(FALSE),
+	                     test_purple_protocol_conversation_create_conversation_normal);
+	g_test_add_data_func("/protocol-conversation/normal/create-conversation-error",
+	                     GINT_TO_POINTER(TRUE),
+	                     test_purple_protocol_conversation_create_conversation_normal);
+
 	/* Normal send message tests. */
 	g_test_add_data_func("/protocol-conversation/normal/send-message-normal",
 	                     GINT_TO_POINTER(FALSE),
