This is a documentation of my reverse-engineering of Dota 2's internal system for creating parties. I will write another guide for the lobby system as well at some point. This guide allows you to write bots that can setup and participate in parties.

Inviting Someone to a Party

Here's the sequence of events, -> indicates outgoing and vise-versa:

 -> CMsgInviteToParty
 <- CacheSubscribe type 2003, id is lobby ID
 <- SOSingleObject type 2003, id is lobby ID
 <- CMsgInvitationCreated group_id is lobby_id, steam_id is target's
 -> CMsgClientUDSInviteToGame steam_id_dest target's, steam_id_src=0, connect_string "+invite lobby_id"

The process is similar to a Dota 2 lobby - there is a cache subscription event with a type of 2 (party) while for lobbies, the type is 4. The object_data is the full party object seen here:

message CSODOTAParty {
    enum State {
        UI = 0;
        FINDING_MATCH = 1;
        IN_MATCH = 2;

    optional uint64 party_id = 1 [(key_field) = true];
    optional fixed64 leader_id = 2;
    repeated fixed64 member_ids = 3;
    optional uint32 game_modes = 4;
    repeated fixed64 pending_invites = 5;
    optional .CSODOTAParty.State state = 6 [default = UI];
    optional uint32 effective_started_matchmaking_time = 7;
    optional uint32 raw_started_matchmaking_time = 32;
    optional uint32 attempt_start_time = 33;
    optional uint32 attempt_num = 34;
    optional uint32 matchgroups = 11;
    optional uint32 low_priority_account_id = 19;
    optional .MatchType match_type = 21 [default = MATCH_TYPE_CASUAL];
    optional .DOTABotDifficulty bot_difficulty = 22 [default = BOT_DIFFICULTY_PASSIVE];
    optional uint32 team_id = 23;
    optional uint32 match_disabled_until_date = 24;
    optional uint32 match_disabled_account_id = 25;
    optional uint32 matchmaking_max_range_minutes = 26;
    optional uint32 matchlanguages = 27;
    optional uint32 map_preference = 38;
    repeated .CSODOTAPartyMember members = 29;
    optional uint32 open_guild_id = 30;
    repeated uint32 common_guilds = 31;
    optional uint32 low_priority_games_remaining = 35;
    optional uint32 min_level = 36;
    optional uint32 max_level = 37;

The initial party is, of course, empty. The CMsgInvitationCreated message adds the player to the list of "waiting for invite" list, however, there is also the pending_invites field, which may also contain the list of outgoing invites (I'm not sure yet).

Here's the invitation created message:

message CMsgInvitationCreated {
    optional uint64 group_id = 1;
    optional fixed64 steam_id = 2;

The group_id field is the party ID. The steam_id field is the SteamID of the target user.

After this is received, the client sends this out:

message CMsgClientUDSInviteToGame {
    optional fixed64 steam_id_dest = 1;
    optional fixed64 steam_id_src = 2;
    optional string connect_string = 3;

The destination SteamID is the invited person, the source steam_id is zero, and the connect_string is +invite party_id, where party_id is the party ID.

The GC invitation created events are for the in-game invite process. The UDSInviteToGame messages are for the Steam style chat invites. The player will see a note over the chat message, which, when clicked, will run the command "+invite lobby_id" in Dota 2, thereby accepting the invite. It is not necessary in the process but works to add another notification for a party invite.

Being Invited to a Party

Sequence of events:

 <- CacheSubscribe, type 2006, id is lobby ID, type is party invite
 <- ClientUDSInviteToGame, identical to outgoing ClientUDSInviteToGame
 -> CMsgPartyInviteResponse, party_id is party ID, etc
 <- Party snapshot (see above)
 <- Unsubscribe from party invite

Party invite in the cache subscription looks like this:

message CSODOTAPartyInvite {
    message PartyMember {
        optional string name = 1;
        optional fixed64 steam_id = 2;

    optional uint64 group_id = 1 [(key_field) = true];
    optional fixed64 sender_id = 2;
    optional string sender_name = 3;
    repeated .CSODOTAPartyInvite.PartyMember members = 4;
    optional uint32 team_id = 5;
    optional bool low_priority_status = 6;
    optional bool as_coach = 7;

All fields are fairly intuitively filled in, except for as_coach, which is always false.

message CMsgPartyInviteResponse {
    optional uint64 party_id = 1;
    optional bool accept = 2;
    optional uint32 client_version = 3;
    optional uint32 team_id = 4;
    optional bool as_coach = 5;
    optional uint32 game_language_enum = 6;
    optional string game_language_name = 7;

as_coach is always false, game_language_enum fields are specific to the language, but for english it is 1, and the name is "english". accept is intuitive, just true/false.

Next the game unsubscribes you from the party invite, and subscribes you to the party object.

Lobby Chat

When the party is set up, the client joins the channel Lobby_lobbyid with a channel_type of DOTAChannelType_Party.

Other Methods

Kicking Someone

message CMsgKickFromParty {
    optional fixed64 steam_id = 1;

Leaving Party

message CMsgLeaveParty {

Become a Coach

message CMsgDOTAPartyMemberSetCoach {
    optional bool wants_coach = 1;


I will update this post with more info if it becomes relevant to me. Queuing for a match will probably be in another post.


Blog Logo

Christian Stewart


comments powered by Disqus

Christian Stewart

Also known as Quantum and Paralin.

Back to Overview