diff --git a/libpurple/protocols/ircv3/purpleircv3capabilities.h b/libpurple/protocols/ircv3/purpleircv3capabilities.h
--- a/libpurple/protocols/ircv3/purpleircv3capabilities.h
+++ b/libpurple/protocols/ircv3/purpleircv3capabilities.h
@@ -39,6 +39,8 @@
 
 G_GNUC_INTERNAL PurpleIRCv3Capabilities *purple_ircv3_capabilities_new(PurpleIRCv3Connection *connection);
 
+G_GNUC_INTERNAL void purple_ircv3_capabilities_start(PurpleIRCv3Capabilities *capabilities);
+
 G_GNUC_INTERNAL gboolean purple_ircv3_capabilities_message_handler(GHashTable *tags, const char *source, const char *command, guint n_params, GStrv params, GError **error, gpointer data);
 
 /**
# HG changeset patch
# Node ID 70e72cd67ae5
# Parent  e128168d9ea5
diff --git a/libpurple/protocols/ircv3/purpleircv3capabilities.c b/libpurple/protocols/ircv3/purpleircv3capabilities.c
--- a/libpurple/protocols/ircv3/purpleircv3capabilities.c
+++ b/libpurple/protocols/ircv3/purpleircv3capabilities.c
@@ -21,6 +21,8 @@
 #include "purpleircv3connection.h"
 #include "purpleircv3core.h"
 
+#define PURPLE_IRCV3_CAPABILITIES_VERSION "302"
+
 enum {
 	PROP_0,
 	PROP_CONNECTION,
@@ -40,6 +42,7 @@
 	SIG_READY,
 	SIG_ACK,
 	SIG_NAK,
+	SIG_DONE,
 	N_SIGNALS,
 };
 static guint signals[N_SIGNALS] = {0, };
@@ -68,6 +71,14 @@
 	}
 }
 
+static void
+purple_ircv3_capabilities_finish(PurpleIRCv3Capabilities *capabilities) {
+	purple_ircv3_connection_writef(capabilities->connection,
+	                               "CAP END");
+
+	g_signal_emit(capabilities, signals[SIG_DONE], 0);
+}
+
 /******************************************************************************
  * PurpleIRCv3Capabilities Implementation
  *****************************************************************************/
@@ -247,6 +258,26 @@
 		G_TYPE_NONE,
 		1,
 		G_TYPE_STRING);
+
+	/**
+	 * PurpleIRCv3Capabilities::done:
+	 * @capabilities: The instance.
+	 *
+	 * Emitted when all of the requested capabilities have been either ack'd or
+	 * nak'd by the server.
+	 *
+	 * Since: 3.0.0
+	 */
+	signals[SIG_DONE] = g_signal_new_class_handler(
+		"done",
+		G_OBJECT_CLASS_TYPE(klass),
+		G_SIGNAL_RUN_LAST,
+		NULL,
+		NULL,
+		NULL,
+		NULL,
+		G_TYPE_NONE,
+		0);
 }
 
 /******************************************************************************
@@ -293,8 +324,7 @@
 		 * we're done with capability negotiation.
 		 */
 		if(capabilities->requests->len == 0) {
-			purple_ircv3_connection_writef(capabilities->connection,
-			                               "CAP END");
+			purple_ircv3_capabilities_finish(capabilities);
 		}
 	}
 
@@ -327,7 +357,7 @@
 	g_free(caps);
 
 	if(capabilities->requests->len == 0) {
-		purple_ircv3_connection_writef(capabilities->connection, "CAP END");
+		purple_ircv3_capabilities_finish(capabilities);
 	}
 
 	return ret;
@@ -349,6 +379,12 @@
 		NULL);
 }
 
+void
+purple_ircv3_capabilities_start(PurpleIRCv3Capabilities *capabilities) {
+	purple_ircv3_connection_writef(capabilities->connection, "CAP LS %s",
+	                               PURPLE_IRCV3_CAPABILITIES_VERSION);
+}
+
 gboolean
 purple_ircv3_capabilities_message_handler(G_GNUC_UNUSED GHashTable *tags,
                                           G_GNUC_UNUSED const char *source,
diff --git a/libpurple/protocols/ircv3/purpleircv3connection.c b/libpurple/protocols/ircv3/purpleircv3connection.c
--- a/libpurple/protocols/ircv3/purpleircv3connection.c
+++ b/libpurple/protocols/ircv3/purpleircv3connection.c
@@ -31,6 +31,12 @@
 };
 static GParamSpec *properties[N_PROPERTIES] = {NULL, };
 
+enum {
+	SIG_REGISTRATION_COMPLETE,
+	N_SIGNALS,
+};
+static guint signals[N_SIGNALS] = {0, };
+
 struct _PurpleIRCv3Connection {
 	PurpleConnection parent;
 
@@ -189,9 +195,6 @@
 		return;
 	}
 
-	purple_connection_set_state(PURPLE_CONNECTION(connection),
-	                            PURPLE_CONNECTION_STATE_CONNECTED);
-
 	g_message("Successfully connected to %s", connection->server_name);
 
 	/* Save our connection and setup our input and outputs. */
@@ -217,12 +220,20 @@
 	                                    connection);
 
 	/* Send our registration commands. */
-	purple_ircv3_connection_writef(connection, "CAP LS %s",
-	                               PURPLE_IRCV3_CONNECTION_CAP_VERSION);
+	purple_ircv3_capabilities_start(connection->capabilities);
 	purple_ircv3_connection_send_user_command(connection);
 	purple_ircv3_connection_send_nick_command(connection);
 }
 
+static void
+purple_ircv3_connection_caps_done_cb(G_GNUC_UNUSED PurpleIRCv3Capabilities *caps,
+                                     gpointer data)
+{
+	PurpleIRCv3Connection *connection = data;
+
+	g_signal_emit(connection, signals[SIG_REGISTRATION_COMPLETE], 0);
+}
+
 /******************************************************************************
  * PurpleConnection Implementation
  *****************************************************************************/
@@ -306,6 +317,16 @@
 	return TRUE;
 }
 
+static void
+purple_ircv3_connection_registration_complete_cb(PurpleIRCv3Connection *connection) {
+	/* Don't set our connection state to connected until we've completed
+	 * registration as connected implies that we can start chatting or join
+	 * rooms and other "online" activities.
+	 */
+	purple_connection_set_state(PURPLE_CONNECTION(connection),
+	                            PURPLE_CONNECTION_STATE_CONNECTED);
+}
+
 /******************************************************************************
  * GObject Implementation
  *****************************************************************************/
@@ -376,7 +397,11 @@
 
 	/* Finally create our objects. */
 	connection->cancellable = g_cancellable_new();
+
 	connection->capabilities = purple_ircv3_capabilities_new(connection);
+	g_signal_connect_object(connection->capabilities, "done",
+	                        G_CALLBACK(purple_ircv3_connection_caps_done_cb),
+	                        connection, 0);
 }
 
 static void
@@ -430,6 +455,29 @@
 		G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);
 
 	g_object_class_install_properties(obj_class, N_PROPERTIES, properties);
+
+	/* Signals */
+
+	/**
+	 * PurpleIRCv3Connection::registration-complete:
+	 * @connection: The instance.
+	 *
+	 * This signal is emitted after the registration process has been
+	 * completed. Plugins can use this to perform additional actions before
+	 * any channels are auto joined or similar.
+	 *
+	 * Since: 3.0.0
+	 */
+	signals[SIG_REGISTRATION_COMPLETE] = g_signal_new_class_handler(
+		"registration-complete",
+		G_OBJECT_CLASS_TYPE(klass),
+		G_SIGNAL_RUN_LAST,
+		G_CALLBACK(purple_ircv3_connection_registration_complete_cb),
+		NULL,
+		NULL,
+		NULL,
+		G_TYPE_NONE,
+		0);
 }
 
 /******************************************************************************
diff --git a/libpurple/protocols/ircv3/purpleircv3core.h b/libpurple/protocols/ircv3/purpleircv3core.h
--- a/libpurple/protocols/ircv3/purpleircv3core.h
+++ b/libpurple/protocols/ircv3/purpleircv3core.h
@@ -27,6 +27,4 @@
 
 #define PURPLE_IRCV3_DOMAIN (g_quark_from_static_string("ircv3-plugin"))
 
-#define PURPLE_IRCV3_CONNECTION_CAP_VERSION "302"
-
 #endif /* PURPLE_IRCV3_CORE_H */
