First, we send a GET request to client.discovery.minecraft-services.net
. The path is dynamically made:
The base is api/v1.0/discovery/MinecraftEdu/builds/
Then we add the version number, for example 1.21.06
So the final url would be: client.discovery.minecraft-services.net/api/v1.0/discovery/MinecraftEdu/builds/1.21.06
This data just gives us some URLs and importantly the playfab title id.
Then, we send a POST request to login.minecrafteduservices.com/initialPacks
{
"build": 12106000, // Just the id without dots and 8 digits long (1.21.06)
"clientVersion": 685, // Not sure
"correlationVector": "a2eb8993-d588-47ff-86ad-7df9f49ecdeb", // Seems to be a GUID
"displayVersion": "1.21.6",
"locale": "en_US",
"osVersion": "10.0.26100",
"platform": "Windows Desktop Build (Centennial)(x64)",
"platformCategory": "desktop"
}
This doesn’t get any data back so I’m not sure what the point of this is.
Then, we see the first use of the PlayFab backend of all this. We send a POST request to 6955f.playfabapi.com
. This seems to be the Minecraft Education API server. The 6955f
part is the titleId
which is found under the actual PlayFab console, meaning it won’t change (at least for a while).
Now we log in using a custom auth method (meaning not google or another method like that). We send another message to 6955f.playfabapi.com
at the /Client/LoginWithCustomID?sdk=XPlatCppSdk-3.6.190304
endpoint. In this case, the URL parameter SDK just sends of a version. Handily, Microsoft provides documentation for this specific endpoint: https://learn.microsoft.com/en-us/rest/api/playfab/client/authentication/login-with-custom-id?view=playfab-rest
It is really great to read. Here is the data sent, and we’ll talk about each parameter in a second.
POST Request data:
{
"CreateAccount": null, // Do not create an account on playfab if there is no ID linked to the CustomID
"CustomId": "REDACTED", // This is UNIQUE to your account... <https://www.gamebackend.dev/2-ways-of-authentication-in-playfab-basics-of-player-login-8a29ee2fd54d>
"EncryptedRequest": null, // Base64 encoded body that is encrypted with the Title's public RSA key (Enterprise Only).
"InfoRequestParameters": { // Read BELOW!
"GetCharacterInventories": false,
"GetCharacterList": false,
"GetPlayerProfile": true,
"GetPlayerStatistics": false,
"GetTitleData": false,
"GetUserAccountInfo": true,
"GetUserData": false,
"GetUserInventory": false,
"GetUserReadOnlyData": false,
"GetUserVirtualCurrency": false,
"PlayerStatisticNames": null,
"ProfileConstraints": null,
"TitleDataKeys": null,
"UserDataKeys": null,
"UserReadOnlyDataKeys": null
},
"PlayerSecret": null, // Player secret that is used to verify API request signatures (Enterprise Only).
"TitleId": "6955F" // PlayFab specific game ID
}
Now, the interesting part is the InfoRequestParameters
. I am not going to go through each parameter, as they can be seen in this section of the documentation. The main bits to note is what the client requests: GetUserAccountInfo
and GetPlayerProfile
. These names don’t make heaps of sense but looking at the response you can easily see what they do:
{ // Note that PlayFabId and PlayerId are the same
"code": 200,
"status": "OK",
"data": {
"SessionTicket": "REDACTED", // An auth ticket used for future auth. Seems to be in the format of PlayerID-PublisherID-TitlePlayerAccount-TitleId- not sure what next
"PlayFabId": "REDACTED", // The ID of your account. This is what CustomId is associated with
"NewlyCreated": false, // True if the master_player_account was newly created on this login.
"SettingsForUser": { // Not 100% sure what these do
"NeedsAttribution": false,
"GatherDeviceInfo": true,
"GatherFocusInfo": true
},
"LastLoginTime": "2025-06-04T06:56:38.585Z", // The time of this user's previous login. If there was no previous login, then it's DateTime.MinValue
"InfoResultPayload": { // The response to our InfoRequestParameters
"AccountInfo": {
"PlayFabId": "REDACTED", // Same as above ^^^^
"Created": "2025-06-04T06:53:35.551Z", // The rest is pretty self explanatory...
"TitleInfo": {
"Origination": "CustomId",
"Created": "2025-06-04T06:53:35.551Z",
"LastLogin": "2025-06-04T06:56:49.066Z",
"FirstLogin": "2025-06-04T06:53:35.551Z",
"isBanned": false,
"TitlePlayerAccount": {
"Id": "REDACTED",
"Type": "title_player_account",
"TypeString": "title_player_account"
}
},
"PrivateInfo": {},
"CustomIdInfo": {
"CustomId": "REDACTED" // Same as in request
}
},
"UserInventory": [], // We don't have this as we are playing a game with no *global* inventory
"UserDataVersion": 0, // The version of the UserData that was returned.
"UserReadOnlyDataVersion": 0, // The version of the Read-Only UserData that was returned.
"CharacterInventories": [], // Again we don't have inventories
"PlayerProfile": { // <https://learn.microsoft.com/en-us/rest/api/playfab/client/authentication/login-with-custom-id?view=playfab-rest#playerprofilemodel>
"PublisherId": "B63A0803D3653643",
"TitleId": "6955F",
"PlayerId": "REDACTED"
}
},
"EntityToken": { // Formerly triggered an Entity login with a normal client login. This is now automatic, and always-on.
"EntityToken": "REDACTED", // The token used to set X-EntityToken for all entity based API calls. Note that this is (or at least some of it is) base64 encoded.
"TokenExpiration": "2025-06-05T06:56:49Z", // The time the token will expire, if it is an expiring token, in UTC.
"Entity": { // The entity id and type.
"Id": "REDACTED", // Unique ID of the entity.
"Type": "title_player_account", // Entity type. See <https://docs.microsoft.com/gaming/playfab/features/data/entities/available-built-in-entity-types>
"TypeString": "title_player_account"
}
},
"TreatmentAssignment": { // The experimentation treatments for this user at the time of login. We will see this later....
"Variants": [],
"Variables": []
}
}
}
Phew! That was a lot to take in…. But it is fundamental information, as we CANNOT do anything without authentication…
We then send a POST request to the same payfabapi server with the path of Authentication/GetEntityToken?sdk=XPlatCppSdk-3.6.190304
. One good thing to notice is that we send the entity token as x-entitytoken
. Here is the JSON request body:
{ // I haven't commented these, as I wrote about this before in the login stage
"Entity": {
"Id": "REDACTED",
"Type": "master_player_account"
}
}
We then get a response:
{
"code": 200,
"status": "OK",
"data": {
"EntityToken": "REDACTED",
"TokenExpiration": "2025-06-05T06:56:49Z",
"Entity": {
"Id": "REDACTED",
"Type": "master_player_account",
"TypeString": "master_player_account"
}
}
}