Reverse Engineering The Halo Infinite Rating And Favoriting API

How to rate and bookmark Halo Infinite assets programmatically.

By Den in Hackery

July 10, 2022

NOTE

This post is part of a series about the Halo Infinite Web API.

You can read more about how I started in the first post, where I talk about the process of figuring out the data endpoints, as well as more about the authentication process.

You can also explore the .NET wrapper for the API that makes endpoint interaction a bit easier.

If you’ve been following me on Twitter, you probably already know that I spend inordinate amounts of time on reverse engineering the Halo Infinite API. As I am working on my .NET wrapper for it (astutely called Grunt), I realized that putting together a nice-to-use blanket over the many GET HTTP APIs is relatively straightforward. There are some permissions here and there that I need to figure out, or in some cases fiddle with undocumented query parameters.

What’s more interesting and a bit more complicating is wrapping any API calls that are implemented as PUT or PATCH requests. For the purposes of this blog post, I wanted to focus on two particular APIs of this flavor - asset rating and favoriting.

Let’s get right to the core of the problem - for someone to rate or favorite an asset, they need to send data to the server that contains that asset information. I have zero clue for what that information should look like. Judging from the settings endpoint that I referenced in my very first investigation (I also maintain an up-to-date list on the OpenSpartan Wiki), there are two API endpoints of interest to us.

For rating an asset, we have HIUGC_RateAnAsset:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
"HIUGC_RateAnAsset": {
    "AuthorityId": "HIUGC_Authoring_Authority",
    "Path": "/hi/players/{player}/ratings/{assetType}/{assetId}",
    "QueryString": "",
    "RetryPolicyId": "linearretry",
    "TopicName": "",
    "AcknowledgementTypeId": 0,
    "AuthenticationLifetimeExtensionSupported": false,
    "ClearanceAware": false
}

Where HIUGC_Authoring_Authority is defined as:

1
2
3
4
5
6
7
8
9
"HIUGC_Authoring_Authority": {
    "AuthorityId": "HIUGC_Authoring_Authority",
    "Scheme": 2,
    "Hostname": "authoring-infiniteugc.svc.halowaypoint.com",
    "Port": 443,
    "AuthenticationMethods": [
        15
    ]
}

Standard authoring endpoint - this is the API surface that manages user-generated content. HIUGC likely stands for “Halo Infinite User Generated Content”, by the way - I have no confirmation that this is the case, but that sounds like a reasonable guess.

Favoriting an asset requires the use of HIUGC_FavoriteAnAsset which is using the exact same HIUGC authority as ratings:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
"HIUGC_FavoriteAnAsset": {
    "AuthorityId": "HIUGC_Authoring_Authority",
    "Path": "/hi/players/{player}/favorites/{assetType}/{assetId}",
    "QueryString": "",
    "RetryPolicyId": "linearretry",
    "TopicName": "",
    "AcknowledgementTypeId": 0,
    "AuthenticationLifetimeExtensionSupported": false,
    "ClearanceAware": false
}

Great. So, from the breakdown above, we have an idea of the endpoints that we need to use to do anything with our assets, such as maps. The question still remains - how do we do this exactly?

To get some insights into the process, I fired up my good friend Fiddler and started looking at outgoing requests every time I try to rate or favorite an asset. Let’s start with ratings.

Fiddler trace of a rating request in Halo Infinite

Bingo - we now see that it’s a PUT request against the endpoint I mentioned above. But what’s up with all that gibberish in the request body? Clearly that looks nothing like structured JSON data that I am so used to for all other requests that I’ve been experimenting with. Recall that the game itself, in the interest of saving bandwidth, does not send JSON or XML data over the wire. Instead, it uses Microsoft Bond - a data packaging format that reduces the amount of data being sent and received by removing as much information from it as possible.

Think of it this way - when you send mail to your friend in California, on the envelope you might indicate “FROM: Jane Doe, 555 Some Street, New York, NY” and “TO: John Doe, 123 Another Street, Malibu, CA”. You clearly labeled what the “FROM” and “TO” addresses are. That’s sending data in JSON. Anyone that looks at the envelope will know what each address means right away, because you labeled the data.

If you use a data packaging format like Microsoft Bond, the labels are eliminated in favor of contextual inference. That is - you have some schema behind the scenes that tells you “This data is in field 0, and this other data is in field 1,” but if you casually look at the data, you might not know what it is. In our envelope example from above, imagine that you don’t have any “FROM” or “TO” labels, but instead write the “TO” address prominently in the center of the envelope, and the “FROM” address in the top right corner. Someone who has no idea how the North American postal system works might not understand what addresses on the envelope represent, but they can try and assume that from context.

That’s what we will do here. Because I know that the data in the rating API is sent in Microsoft Bond format, I can save the response body through Fiddler and try and parse it out with the utility snippet I wrote.

Decoded Microsoft Bond responses using a custom-built utility to parse encoded data

Alright, so this looks interesting - some numbers and what looks like the star rating that I set (3 stars). From the parsed request, it seems that there are two properties - one struct that carries the undetermined numbers and another that contains the rating. Good starting point, but I am still not sure what to make of the numbers.

Let’s take a look at the PUT API request URL here once again when I am trying to rate an asset:

1
2
3
4
5
6
7
https://authoring-infiniteugc.svc.halowaypoint.com
    /hi
    /players
    /xuid(2533274855333605)
    /ratings
    /UgcGameVariants
    /4d0f6e15-cc3f-46e0-9d06-22de6311c4cb

From the request URL, the following information is passed:

  • My Xbox ID (XUID), meaning that the asset is rated by my Xbox Live ID.
  • The type of the asset being rated (UgcGameVariant).
  • The asset ID (4d0f6e15-cc3f-46e0-9d06-22de6311c4cb).

There must be some other information passed in the request that is not captured in the URL, or at least that is my assumption - why have redundant data moving around? Now to figure out what that data could be.

Let’s say for a moment that instead of trying to set the rating, I am trying to get the rating that I set up for a given asset. I would use the exact same URL as above, but would instead send a GET request. If I did that, I’d get the following as a response:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
{
    "Links": {},
    "Name": "",
    "Description": "",
    "AssetId": "4d0f6e15-cc3f-46e0-9d06-22de6311c4cb",
    "AssetVersionId": "d6a43d27-9a54-4873-b7b5-fb2c22539fdc",
    "CustomData": {
        "Score": 3
    },
    "VersionRatings": [
        {
            "AssetVersionId": "ad7c1591-fe57-4ef7-8b0b-a4ba861fd8d7",
            "Score": 0,
            "LastModified": {
                "ISO8601Date": "2022-07-10T21:40:56.308Z"
            }
        },
        {
            "AssetVersionId": "d6a43d27-9a54-4873-b7b5-fb2c22539fdc",
            "Score": 3,
            "LastModified": {
                "ISO8601Date": "2022-07-10T22:34:02.961Z"
            }
        }
    ],
    "AssetKind": 6
}

From the list above, I can see that we have the asset ID repeated in the response, which makes sense - I am getting it as metadata. There are also dates set for each rating interaction, but I somehow don’t think that would be determined by the client, so the numbers we saw in the Bond-decoded response are unlikely to be dates. This leaves us with AssetVersionId.

In Halo Infinite, different assets might have different versions, which are marked with their very own version GUIDs. It’s possible that the GUID is passed in the encoded Bond response for the version for which the rating is applied, but none of the numbers themselves look like GUIDs. What gives? The key is in the data formats. Notice the following breakdown of values:

  • BT_UINT32: 3601087783
  • BT_UINT16: 39508
  • BT_UINT16: 18547
  • BT_UINT64: 15897516615889827255

We have a sequence of 4 bytes, 2 bytes, 2 bytes, and 8 bytes. 16 bytes total. That sure does look like a GUID. In a very hacky way, we can try and “assemble” the data with this little C# snippet:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
using System;

class Program {
    static void Main(string[] args) {
      UInt32 sequenceA = 3601087783;
      UInt16 sequenceB = 39508;
      UInt16 sequenceC = 18547;
      UInt64 sequenceD = 15897516615889827255;
      
      byte[] data = new byte[16];

      var firstPart = BitConverter.GetBytes(sequenceA);
      var midPart = BitConverter.GetBytes(sequenceB);
      var midToLastPart = BitConverter.GetBytes(sequenceC);
      var lastPart = BitConverter.GetBytes(sequenceD);

      Array.Copy(firstPart, 0, data, 0, 4);
      Array.Copy(midPart, 0, data, 4, 2);
      Array.Copy(midToLastPart, 0, data, 6, 2);
      Array.Copy(lastPart, 0, data, 8, 8);
      
      var guid = new Guid(data);
      Console.WriteLine(guid);
    }
}

What this snippet does is convert the values to their respective byte array representation and then assemble the chunks into one.

The result?

1
d6a43d27-9a54-4873-b7b5-fb2c22539fdc

This matches exactly our AssetVersionId from the API response above! Can we call this an almost-Eureka? Only because we still do not know how to format the data if we pass it to the PUT request as anything other than a Bond request. If I wanted to, I could just packaged the information that I have about the structure in Bond and send it to the service to set the rating, but that would be too easy. Let’s figure out how to do it with JSON, because my hunch is that this API exists as well.

To set the rating, we need to pass the version and the rating number. Looking at the VersionRatings snippet above, this seems like what we need to pass:

1
2
3
4
5
6
7
{
    "AssetVersionId": "d6a43d27-9a54-4873-b7b5-fb2c22539fdc",
    "Score": 3,
    "LastModified": {
        "ISO8601Date": "2022-07-10T23:26:56.63Z"
    }
}

Minus the date, of course, because it’s clearly not included in the Bond-encoded request and we don’t need it here either. Which leaves us with:

1
2
3
4
{
    "AssetVersionId": "d6a43d27-9a54-4873-b7b5-fb2c22539fdc",
    "Score": 3
}

Doing this inside a PUT request, however, reset my score to zero:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
{
    "Links": {},
    "Name": "",
    "Description": "",
    "AssetId": "4d0f6e15-cc3f-46e0-9d06-22de6311c4cb",
    "AssetVersionId": "d6a43d27-9a54-4873-b7b5-fb2c22539fdc",
    "CustomData": {
        "Score": 0
    },
    "VersionRatings": [
        {
            "AssetVersionId": "ad7c1591-fe57-4ef7-8b0b-a4ba861fd8d7",
            "Score": 0,
            "LastModified": {
                "ISO8601Date": "2022-07-10T21:40:56.308Z"
            }
        },
        {
            "AssetVersionId": "d6a43d27-9a54-4873-b7b5-fb2c22539fdc",
            "Score": 0,
            "LastModified": {
                "ISO8601Date": "2022-07-10T23:28:46.648Z"
            }
        }
    ],
    "AssetKind": 6
}

Not ideal, and clearly not what I wanted. Maybe I just need to include the score and omit the version altogether? That didn’t pan out too well, but I did get a clue in the response message:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
{
    "errors": {
        "AssetVersionId": [
            "AssetVersionId cannot be null when creating a Rating PlayerLink."
        ]
    },
    "title": "One or more validation errors occurred.",
    "status": 400,
    "traceId": "SOME_LONG_ID"
}

We’re on the right track - AssetVersionId needs to be there, and so does the score, but my initial envelope didn’t quite work. Where else is the score captured in the original API response (when I queried the ratings)? This snippet:

1
2
3
"CustomData": {
    "Score": 3
}

What if we combine that with the version ID and pass it to the service, giving us this envelope instead:

1
2
3
4
5
6
{
    "AssetVersionId": "d6a43d27-9a54-4873-b7b5-fb2c22539fdc",
    "CustomData": {
        "Score": 3
    }
}

Amazingly, this worked!

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
{
    "Links": {},
    "Name": "",
    "Description": "",
    "AssetId": "4d0f6e15-cc3f-46e0-9d06-22de6311c4cb",
    "AssetVersionId": "d6a43d27-9a54-4873-b7b5-fb2c22539fdc",
    "CustomData": {
        "Score": 3
    },
    "VersionRatings": [
        {
            "AssetVersionId": "ad7c1591-fe57-4ef7-8b0b-a4ba861fd8d7",
            "Score": 0,
            "LastModified": {
                "ISO8601Date": "2022-07-10T21:40:56.308Z"
            }
        },
        {
            "AssetVersionId": "d6a43d27-9a54-4873-b7b5-fb2c22539fdc",
            "Score": 3,
            "LastModified": {
                "ISO8601Date": "2022-07-10T23:31:44.187Z"
            }
        }
    ],
    "AssetKind": 6
}

I noticed that the rating was set for the version ID that I expected, and the time matched the time of me sending the request. Now I can say “Eureka!” for real. We now know how to rate Halo Infinite assets programmatically without having to Bond-encode our requests.

Now, to the favoriting API. As I explored the rating API, I thought that the favoriting behavior must be at least somewhat similar. I use “favoriting” as the term used in the API definition - I realize that in game it’s called “bookmarking.”

Using the API assumptions above (remember HIUGC_FavoriteAnAsset) I tracked down the exact API call in Fiddler once again. Its structure is as follows:

1
2
3
4
5
6
7
https://authoring-infiniteugc.svc.halowaypoint.com
      /hi
      /players
      /xuid(2533274855333605)
      /favorites
      /UgcGameVariants
      /f96f57e2-9f15-45c5-83ac-5775a48d2ba8

It’s also a PUT request with a bunch of gibberish captured in Fiddler as the request body. However, saving the data and decoding it through my tiny utility I get this:

Decoded Bond structure for the request to favorite a Halo Infinite asset

This looks a bit too familiar, with the only difference that it seems that only the version GUID is passed to the request. Crafting a PUT request with the following content for the game variant, then:

1
2
3
{
    "AssetVersionId": "2674c887-7aa1-42ab-a6cd-4a2c60611d0e"
}

Sure enough, that returned the expected result:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
{
    "Links": {},
    "Name": "",
    "Description": "",
    "AssetId": "f96f57e2-9f15-45c5-83ac-5775a48d2ba8",
    "AssetVersionId": "2674c887-7aa1-42ab-a6cd-4a2c60611d0e",
    "CustomData": {},
    "VersionRatings": null,
    "AssetKind": 6
}

I now have the asset favorited. But what about deleting? When we were tweaking ratings, it’s very easy to adjust the value because there is an explicit property for it. Favoriting is different because we have nothing like:

1
"IsBookmarked": true

Turns out the folks at 343 really did think through their REST API - to remove a favorite (i.e., a bookmark), just send the request body with the version, but instead of PUT use DELETE. If the request is successful, you will get a 204 No Content response, and the favorited asset will be removed.

I’ve verified all the behaviors above in-game and confirmed that both ratings and favorites are correctly propagating.

There you have it - you now know how to manage ratings and favorites for your Halo Infinite account outside the game itself.

Want to get more notes like the above? Subscribe to The Den!

A monthly newsletter about product management, engineering, and tinkering with code.

Feedback

Have any thoughts? Let me know on Twitter!