User:Dango360

From Archiveteam
Jump to navigation Jump to search

Dango’s Roblox Archiving Notes (DRAFT, last updated 31/01/2024)

Well, someone has to go crazy in-depth on how Roblox’s cogs work. Looks like it’s me!

While I will be talking about Roblox’s web APIs, I won’t be going into depth about how the website itself works since I’m not a connoisseur of those kinds of things (I’m guessing it’s not too difficult to grab what’s needed to archive the site). These notes are more in the theory than doing the thing; ideas are laid out on what to do. Also, forgive me if I sound naive in some parts or seem to de-advance in certain areas. I’m learning how this all works as I’m typing this out so it’s bound to be fragmented.

Any feedback can go into User talk:Dango360. All I’m typing on this page will mostly be informal, which can be taken away with minimal impact on how it’s read.

The curl commands and responses are in the notes as well, so examples can be easily followed and replicated. At the time of writing these, all the APIs on this page should work. If an API becomes unavailable, it’s most likely Roblox discontinued the API in favour of a more updated one.

ChatGPT was not used in the making of this document. B)

Things to know

PageCursors

A PageCursors is Base64 encoded JSON text. Once decoded, the JSON text with a 64-character hash is available.

From https://users.roblox.com/v1/users/85382088/username-history?limit=100&sortOrder=Asc:



eyJrZXkiOjUwNzIyOSwic29ydE9yZGVyIjoiQXNjIiwicGFnaW5nRGlyZWN0aW9uIjoiQmFja3dhcmQiLCJwYWdlTnVtYmVyIjoxLCJkaXNjcmltaW5hdG9yIjoidXNlcklkOjg1MzgyMDg4IiwiY291bnQiOjEwMH0KY2QyNWFkYmNlODdiMTg0YmRkZTNjNzJkYjdiMzM0Zjg0NGJjZjY2MzNmZmViMzc2MGUwODBjMzhjYjU3MWVmMA==

⬇️

{"key":507229,"sortOrder":"Asc","pagingDirection":"Backward","pageNumber":1,"discriminator":"userId:85382088","count":100}
cd25adbce87b184bdde3c72db7b334f844bcf6633ffeb3760e080c38cb571ef0

  • It is unknown what purpose the key “key” has. Maybe it’s counting how many pageCursors have been made? (Half a million is not a lot, though…)
  • The hash is not from the JSON text itself. It is still being determined where the hash comes from.
  • The cursors are currently non-replicable on the client and must be requested/generated from Roblox sequentially (Page 1, Page 2, Page 3, etc.) before the pages can be grabbed.
  • They can be reused at a later date. The duration until the cursor expires is currently unknown.
  • If there are no more items available to show, the nextPageCursor will be “null”.

Asc or Desc sortOrder?

For the sortOrder, should we do Ascending or Descending? That is the question.

While ascending gives us a more natural way of reading the data (first to last), descending will allow us to utilise page cursor item separation in the tracker.

The cursors could be shared in a pool, rather than one user being forced to download the whole thing (and watching it give 404s after going through almost 4 hours of grabbing all of the metadata and the program deciding to not write to WARC anymore, wasting their time. cough cough, blogspot blog:itemwiththousandsofpages).

Anyways, it will keep the load light on all accounts and make for quick down-chiving (a portmanteau for downloading and archiving). It would also allow us to keep track of every new addition by accessing the last page that was grabbed that gave a null nextCursorPage.

We could do both orders, but it would be a very easy way to confuse us in case a bug causes things to go horribly, horribly wrong. It would also be double the workload for the same kind of data. Case in point, we stick to one order and stay in that order for the whole project.

*.rbxcdn.com

rbxcdn.com is Roblox’s (rbx) Content Delivery Network (CDN). Simple.

The sub-domains the CDN uses: (https://www.eff.org/https-everywhere/atlas/domains/rbxcdn.com.html)

  • tr.rbxcdn.com
    • Generated images of Avatars, essentially UGC decals/images. Mostly used by Roblox’s website. Unknown if it is used at all on the client/studio software.

These sub-domains hold the content of Roblox. These are the meat and potatoes when archiving Roblox. These can have sound, images, models, places; basically anything. There are 8 of these:

  • c0.rbxcdn.com
  • c1.rbxcdn.com
  • c2.rbxcdn.com
  • c3.rbxcdn.com
  • c4.rbxcdn.com
  • c5.rbxcdn.com
  • c6.rbxcdn.com
  • c7.rbxcdn.com

Unique sub-domains that are still online include:

  • c0ak.rbxcdn.com
    • Testing subdomain? (Doesn’t seem to be used right now)
  • apis.rbxcdn.com
    • Currently for autocompleting string queries(?)
  • css.rbxcdn.com
    • Cascading Style Sheets for the website
  • js.rbxcdn.com
    • Javascript for the website
  • images.rbxcdn.com
    • Roblox-managed website assets
  • static.rbxcdn.com
    • More Roblox-managed website assets

t* used to hold website images, a bit like tr but in 8 separate subdomains. However, they have been defunct for a while (2019-ish), but the domains still work and might still be utilised in certain areas. If they are, please let me know.

  • t0.rbxcdn.com
  • t1.rbxcdn.com
  • t2.rbxcdn.com
  • t3.rbxcdn.com
  • t4.rbxcdn.com
  • t5.rbxcdn.com
  • t6.rbxcdn.com
  • t7.rbxcdn.com

Most, if not all files (yes I know about the hidden server software) hosted on the c* CDNs utilise a random hash with no file extension. So, how can we get to them?

The assetdelivery method

To get to a file, we need the assetId of that file and GET request to https://assetdelivery.roblox.com/v2/assetId/*. This will give us the file’s location, which we can then easily down-chive.

If we can access previous versions of an assetId (for example, uncopylocked places, models and anything similar to a model), we can GET request https://assetdelivery.roblox.com/v2/assetId/*/version/* and count upwards. I will talk more about how this works later. This doesn’t need a pageCursor at all, which means we can down-chive at any time with any user in the pool.

The response should look like this JSON:

{
    "locations": [
        {
            "assetFormat": "source",
            "location": "https://c0.rbxcdn.com/693a41a996e0e9225c50683d3cd771f8"
        }
    ],
    "requestId": "638399616602945164",
    "IsHashDynamic": false,
    "IsCopyrightProtected": false,
    "isArchived": false,
    "assetTypeId": 9
}

The URL to download the file should be in the “location” key.

Batch POST method

To access IDs that don’t allow the GET request (sounds for example), we need to POST request https://assetdelivery.roblox.com/v1/assets/batch with valid IDs that link to a rbxcdn.com URL.

I’m currently looking at https://github.com/Roblox-Devs/RobloxGameScraper/blob/main/app/places/index.js, and it seems that it can also ask for every version available but all at once. This could be the best way to get all of the versions’ URLs at once, but it would not be good for visiting at a later point in the Wayback Machine.

I have no idea how the WM deals with POST, but I’m guessing that it’s not great since having to deal with different request JSONs would complicate things. Still, if the WARC format can save these POST requests the way they were requested, then it could be easy to reuse these later in a different way.

So, which one should be used? While the tracker software works best with smaller chunks, a version of an asset might need to wait for a while before it’s archived. However, assuming that the location URLs don’t expire, the batch method could allow us to grab as many rbxcdn links as we can and archive it.

With the batch method, we could have a pre-grabbing discovery; down-chive as much metadata and content URLs as possible so that when the grab project begins, the metadata and content URLs that we already have can be used. (Note that I’m saying finding the content URLs, not downloading them. Some assets have loads of versions, meaning huge filesizes.)

Although seeing how long it took for Blogger from when the discovery part ended and the grabbing part started (2023 - 2015 = 8), it seems that it could cause some irrational “it’s still up and we did request a lot of data from them; let’s take a small break and deal with the other content later” thoughts.

However, if the content locations expire after request, we could do a hybrid of some sort. As a worker, find out the total amount of versions an asset ID item has. Once the highest number has been found, put all of the versions into a batch POST request. With those content URLs, send each of them off to the tracker for everyone else to download.

AssetTypeId

According to the Roblox Wiki, there are eight different ID systems:

  • Users
  • Groups
  • Assets
  • Bundles
  • Gamepasses
  • Badges
  • Universes
  • And Developer Products

Most of them are sequential (I’ll get to that later…) This allows for quick dumb archiving to take place. This also means that we can do stuff like “user:ID” and “group:ID” for the tracker.

So what does this have to do with AssetTypeId? Well, the asset ID is a bit of a wildcard. It can have decals, images, games, catalog items and many, many more.

In fact, Roblox Wiki lists over 80 different types of assets that an asset ID could be. It would be very tedious to separate those 80 items into 80 different tracker types. Instead, we can find out what type of asset it is and program functions to archive that type.

This means that “asset:ID” is all we would need for all of the asset types. Sometimes we need to use specific methods for specific types. As previously said, the methods needed will differ between certain types (audio…) so identifying what kind of ID it is is crucial in correctly grabbing stuff off of Roblox’s servers.

The easy way is by doing “https://www.roblox.com/library/assetID”, as it would automatically redirect to the correct page (/games and /catalog, for example). If it’s still in “/library”, we can use 7z99’s solution and send a GET request to “https://economy.roblox.com/v2/assets/assetID/details”, which will give us the “AssetTypeId” (it may also have an “IconImageAssetId”, so keep an eye on that if planning to grab as many images as possible).

Note that the API only contains the generic metadata by design and won’t contain any special values that a specific asset type would have. To get that information, you would need to use a corresponding API for that type.

Here is an example of the economy API:



curl:

curl 'https://economy.roblox.com/v2/assets/15636/details' \
  -H 'authority: economy.roblox.com' \
  -H 'accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7' \
  -H 'accept-language: en-GB,en;q=0.9' \
  -H 'cache-control: max-age=0' \
  -H 'dnt: 1' \
  -H 'sec-ch-ua: "Not_A Brand";v="8", "Chromium";v="120", "Google Chrome";v="120"' \
  -H 'sec-ch-ua-mobile: ?0' \
  -H 'sec-ch-ua-platform: "Windows"' \
  -H 'sec-fetch-dest: document' \
  -H 'sec-fetch-mode: navigate' \
  -H 'sec-fetch-site: none' \
  -H 'sec-fetch-user: ?1' \
  -H 'upgrade-insecure-requests: 1' \
  -H 'user-agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36' \
  --compressed

Response body:

{
    "TargetId": 15636,
    "ProductType": "User Product",
    "AssetId": 15636,
    "ProductId": 42529,
    "Name": "Doom Spire",
    "Description": "I think I fixed the regenerate problem.",
    "AssetTypeId": 10,
    "Creator": {
        "Id": 3417,
        "Name": "Leeav",
        "CreatorType": "User",
        "CreatorTargetId": 3417,
        "HasVerifiedBadge": false
    },
    "IconImageAssetId": 0,
    "Created": "2006-11-03T01:56:46.167Z",
    "Updated": "2006-11-03T01:56:46.167Z",
    "PriceInRobux": null,
    "PriceInTickets": null,
    "Sales": 0,
    "IsNew": false,
    "IsForSale": false,
    "IsPublicDomain": true,
    "IsLimited": false,
    "IsLimitedUnique": false,
    "Remaining": null,
    "MinimumMembershipLevel": 0,
    "ContentRatingTypeId": 0,
    "SaleAvailabilityLocations": null,
    "SaleLocation": null,
    "CollectibleItemId": null,
    "CollectibleProductId": null,
    "CollectiblesItemDetails": null
}

Response headers:

Cache-Control:
no-cache
Content-Length:
728
Content-Type:
application/json; charset=utf-8
Date:
Fri, 05 Jan 2024 01:30:45 GMT
Nel:
{"report_to":"network-errors","max_age":604800,"success_fraction":0.001,"failure_fraction":1}
c:
CP="CAO DSP COR CURa ADMa DEVa OUR IND PHY ONL UNI COM NAV INT DEM PRE"
Report-To:
{"group":"network-errors","max_age":604800,"endpoints":[{"url":"https://ncs.roblox.com/upload"}]}
Roblox-Machine-Id:
CHI1-WEB9047
Server:
Microsoft-IIS/10.0
Strict-Transport-Security:
max-age=3600
X-Frame-Options:
SAMEORIGIN
X-Powered-By:
ASP.NET
X-Roblox-Edge:
lhr2
X-Roblox-Region:
us-central

(Just to let you know, https://economy.roblox.com/v2/developer-products/assetId/info gives the same info)

But wait, No. 21 is badges, 32 is bundles and 34 is gamepasses.

Why is that? I have no idea.

Rate limiting the tracker

We should be very careful in grabbing these items. Creating an incident the size or bigger than the October 2021 outage would raise Roblox’s suspicion about what’s going on.

While it would be best to archive all of this when Roblox hypothetically shuts down, it would probably be a long time until this happens and the amount of things to get would be ten-fold (not to mention the kinds of updates Roblox can enforce to stop mass-scraping of certain assets). If we can get as much as we can now, we will have a huge head start in keeping most the platform archived and protected from Roblox's any further modifications.

So, making sure that we don’t overload Roblox at all is vital.

Users

User Profiles

URLs in which the user was terminated/deleted will redirect to a 404 error URL.

Example:



https://www.roblox.com/users/1426/profile

⬇️

https://www.roblox.com/request-error?code=404



User Metadata JSON

Users Api v1 is the API for getting information on the user.

Sending a GET request on https://users.roblox.com/v1/users/{USERID} will give you a JSON response.

Example



curl:

curl -X 'GET' \
  'https://users.roblox.com/v1/users/1' \
  -H 'accept: application/json'

Response body:

{
  "description": "Welcome to the Roblox profile! This is where you can check out the newest items in the catalog, and get a jumpstart on exploring and building on our Imagination Platform. If you want news on updates to the Roblox platform, or great new experiences to play with friends, check out blog.roblox.com. Please note, this is an automated account. If you need to reach Roblox for any customer service needs find help at www.roblox.com/help",
  "created": "2006-02-27T21:06:40.3Z",
  "isBanned": false,
  "externalAppDisplayName": null,
  "hasVerifiedBadge": true,
  "id": 1,
  "name": "Roblox",
  "displayName": "Roblox"
}

Response headers:

 cache-control: no-cache 
 content-length: 601 
 content-type: application/json; charset=utf-8 
 date: Sun,31 Dec 2023 14:26:52 GMT 
 nel: {"report_to":"network-errors","max_age":604800,"success_fraction":0.001,"failure_fraction":1} 
 report-to: {"group":"network-errors","max_age":604800,"endpoints":[{"url":"https://ncs.roblox.com/upload"}]} 
 roblox-machine-id: bde7d3ca2bc8 
 server: Kestrel 
 strict-transport-security: max-age=3600 
 x-frame-options: SAMEORIGIN 
 x-roblox-edge: lhr2 
 x-roblox-region: us-central 

User Avatar

We can what the user is currently wearing on the avatar via https://avatar.roblox.com/v1/users/UserID/currently-wearing.

Avatar headshots can be grabbed via https://thumbnails.roblox.com/v1/users/avatar-headshot?size=48x48&format=png&userIds=USERID. You can have multiple user IDs by adding another &userIds=* to the query.

Username Changes

  • To get every username a User ID had, send a GET request to https://users.roblox.com/v1/users/{USERID}/username-history?limit=100&sortOrder=Desc
  • Note that if there are more than 100 items, a nextPageCursor (Base64) will be supplied in every response (with the last one giving null).
  • You can reach the next page by adding the &cursor= query to the request. It will need to be sequentially done; you cannot reach page 5 if you are on page 2. You must get the cursors for pages 3 and 4 to access 5.

Example



curl:

curl -X 'GET' \
  'https://users.roblox.com/v1/users/85382088/username-history?limit=100&sortOrder=Asc' \
  -H 'accept: application/json'

Response body:

{
  "previousPageCursor": null,
  "nextPageCursor": "eyJrZXkiOjUwNzIyNywic29ydE9yZGVyIjoiQXNjIiwicGFnaW5nRGlyZWN0aW9uIjoiRm9yd2FyZCIsInBhZ2VOdW1iZXIiOjIsImRpc2NyaW1pbmF0b3IiOiJ1c2VySWQ6ODUzODIwODgiLCJjb3VudCI6MTAwfQpiZDJjYzZiMGYyOGRiMmRkMWYyNTc1YTM2ODRmZjkwNWZlMDAyOGFiOWYwODQ1Yzk2NTU2ODFkY2MyNDZhOTM1",
  "data": [
    {
      "name": "xboxvsgunfanboy"
    },
    {
      "name": "OfficiaIKate"
    },
    {
      "name": "xboxvsgunfanboy"
    },
    {
      "name": "OfficiaIKate"
    },
    {
      "name": "BareIyAlive"
    },
    {
      "name": "Fierzaa"
    },
    {
      "name": "Weebzaa"
    },
    {
      "name": "Fierzaa"
    },
    {
      "name": "Aierzaa"
    },
    {
      "name": "BierzaaIsTaken"
    },
    {
      "name": "Cierzaa"
    },
    {
      "name": "DierzaaIsTakenToo"
    },
    {
      "name": "Eierzaa"
    },
    {
      "name": "Fierzaa"
    },
    {
      "name": "Gierzaa"
    },
    {
      "name": "Hierzaa"
    },
    {
      "name": "Ierzaa"
    },
    {
      "name": "Jierzaa"
    },
    {
      "name": "Kierzaa"
    },
    {
      "name": "Lierzaa"
    },
    {
      "name": "Mierzaa"
    },
    {
      "name": "Nierzaa"
    },
    {
      "name": "Oierzaa"
    },
    {
      "name": "Pierzaa"
    },
    {
      "name": "Qierzaa"
    },
    {
      "name": "Rierzaa"
    },
    {
      "name": "Sierzaa"
    },
    {
      "name": "Tierzaa"
    },
    {
      "name": "Uierzaa"
    },
    {
      "name": "VierzaaIsTaken"
    },
    {
      "name": "WierzaaIsAlsoTaken"
    },
    {
      "name": "Xierzaa"
    },
    {
      "name": "Yierzaa"
    },
    {
      "name": "Zierzaa"
    },
    {
      "name": "Trumperis"
    },
    {
      "name": "Fierzeris"
    },
    {
      "name": "Freezeris"
    },
    {
      "name": "DontTrustFierzaa"
    },
    {
      "name": "Fierzaa"
    },
    {
      "name": "KAAAAATE"
    },
    {
      "name": "AlgorithmIntensity"
    },
    {
      "name": "MissStealYoWaifu"
    },
    {
      "name": "OVERVVATCH"
    },
    {
      "name": "ShadowEmpyreus"
    },
    {
      "name": "Fierzaa"
    },
    {
      "name": "AnotherNameChange"
    },
    {
      "name": "loleriszaa"
    },
    {
      "name": "Prismanzaa"
    },
    {
      "name": "Taymasterzaa"
    },
    {
      "name": "Nikiliszaa"
    },
    {
      "name": "Zyzrzaa"
    },
    {
      "name": "Shedletskyzaa"
    },
    {
      "name": "CantStopChangingName"
    },
    {
      "name": "Fierzaa"
    },
    {
      "name": "Dowidzenia"
    },
    {
      "name": "Fierzaa"
    },
    {
      "name": "FierzaaChan"
    },
    {
      "name": "Fierzaa"
    },
    {
      "name": "xboxvsgunfangirI"
    },
    {
      "name": "MyBackHurtsBrb"
    },
    {
      "name": "AColdOneWithTheBoyz"
    },
    {
      "name": "GetOutOfMyBoard"
    },
    {
      "name": "ISeriouslyNeedHelp"
    },
    {
      "name": "KingKakaIsMyPapa"
    },
    {
      "name": "Weebzaa"
    },
    {
      "name": "NotFrieza"
    },
    {
      "name": "Fierzaa"
    },
    {
      "name": "Bierezaa"
    },
    {
      "name": "Ferezaaa"
    },
    {
      "name": "Sierzaa"
    },
    {
      "name": "BigSmokezaa"
    },
    {
      "name": "FierTheReaper"
    },
    {
      "name": "DontFierTheReaper"
    },
    {
      "name": "Fearzaa"
    },
    {
      "name": "FIERZAA"
    },
    {
      "name": "AaahIHateCapsLock"
    },
    {
      "name": "Fierzaa"
    },
    {
      "name": "Barzaar"
    },
    {
      "name": "Fierzaa"
    },
    {
      "name": "ZacAttackkWannabe"
    },
    {
      "name": "Fierzaa"
    },
    {
      "name": "ILoveWastingmoney"
    },
    {
      "name": "Fierzaa1IsTaken"
    },
    {
      "name": "Fierzaa2IsTaken"
    },
    {
      "name": "Fierzaa3IsTaken"
    },
    {
      "name": "Fierzaa4IsTaken"
    },
    {
      "name": "Fierzaa5"
    },
    {
      "name": "Fierzaa6"
    },
    {
      "name": "Fierzaa7IsTakenWOW"
    },
    {
      "name": "Fierzaa8"
    },
    {
      "name": "Fierzaa9"
    },
    {
      "name": "Fierzaa10"
    },
    {
      "name": "WasteOf10kRightThere"
    },
    {
      "name": "Fierzaa"
    },
    {
      "name": "Zombzaa"
    },
    {
      "name": "YourTooSlovv"
    },
    {
      "name": "CheckOutMyTwitterPlz"
    },
    {
      "name": "Fierzaa"
    },
    {
      "name": "lolerisIsTooSlow"
    },
    {
      "name": "Fierzaa"
    }
  ]
}

Response headers:

 cache-control: no-cache 
 content-length: 2563 
 content-type: application/json; charset=utf-8 
 date: Sun,31 Dec 2023 14:35:47 GMT 
 nel: {"report_to":"network-errors","max_age":604800,"success_fraction":0.001,"failure_fraction":1} 
 report-to: {"group":"network-errors","max_age":604800,"endpoints":[{"url":"https://ncs.roblox.com/upload"}]} 
 roblox-machine-id: d5e438b77285 
 server: Kestrel 
 strict-transport-security: max-age=3600 
 x-frame-options: SAMEORIGIN 
 x-roblox-edge: lhr2 
 x-roblox-region: us-central 

User Inventories

Users can choose to keep their inventory private. This means that we cannot know what things a user with a private inventory has created/bought. This doesn't affect getting assets, as the IDs are sequential (at least, for now. See #Random Number update for more.)

[W.I.P]

User Collected Badges

We can get every badge a user has collected with this badge API:

[W.I.P]

Badges

[W.I.P]

Random Number update

Around November 11th, badges went through an update where any new badge ID would be random instead of sequential. (https://twitter.com/Bloxy_News/status/1723384385532260748) (https://devforum.roblox.com/t/creating-a-new-badge-on-my-game-just-gave-me-a-ridiculously-long-badge-id/2694534)

For example, https://www.roblox.com/badges/1640069665114383 uses a randomized number to obfuscate the real number of badges.

  • This signals Roblox’s beginning of the end of sequential ID usage.
  • While the IDs before then are staying the same, it is now impossible to grab every badge from just counting.
  • Other IDs (assets, places, universes, etc.) are still sequential, but it is a matter of time until they’re randomized for new items.
  • Scouring for new items after the final sequential ID in the database will require a list of URLs.

Universes

A universe groups a bunch of places. A universe has a starting place, which allows a user to join the experience. A place in the universe can teleport users to other places in that universe.

To get a Universe ID from a Place/Asset ID, you can request this URL:



curl:

curl 'https://apis.roblox.com/universes/v1/places/1818/universe' \
  -H 'authority: apis.roblox.com' \
  -H 'accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7' \
  -H 'accept-language: en-GB,en;q=0.9' \
  -H 'cache-control: max-age=0' \
  -H 'dnt: 1' \
  -H 'sec-ch-ua: "Not_A Brand";v="8", "Chromium";v="120", "Google Chrome";v="120"' \
  -H 'sec-ch-ua-mobile: ?0' \
  -H 'sec-ch-ua-platform: "Windows"' \
  -H 'sec-fetch-dest: document' \
  -H 'sec-fetch-mode: navigate' \
  -H 'sec-fetch-site: none' \
  -H 'sec-fetch-user: ?1' \
  -H 'upgrade-insecure-requests: 1' \
  -H 'user-agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36' \
  --compressed

Response body:

{"universeId":13058}

Response headers:

Content-Length:
20
Content-Type:
application/json; charset=utf-8
Date:
Thu, 04 Jan 2024 15:13:28 GMT
Nel:
{"report_to":"network-errors","max_age":604800,"success_fraction":0.001,"failure_fraction":1}
Report-To:
{"group":"network-errors","max_age":604800,"endpoints":[{"url":"https://ncs.roblox.com/upload"}]}
Server:
envoy
Strict-Transport-Security:
max-age=3600
X-Envoy-Upstream-Service-Time:
2
X-Roblox-Edge:
lhr2
X-Roblox-Region:
us-central

Places

To grab the metadata of a place, we can use many different APIs. We can use the “https://economy.roblox.com/v2/assets/assetID/details” as stated in AssetTypeId, but it won’t give us all of the place-related information (“VisitedCount” and “TotalUpVotes”, for example). For that, we need to use another API for places.

The most rudimentary one, https://www.roblox.com/places/api-get-details?assetId=*, can be used to grab the metadata:



curl:

curl 'https://www.roblox.com/places/api-get-details?assetId=1818' \
  -H 'authority: www.roblox.com' \
  -H 'accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7' \
  -H 'accept-language: en-GB,en;q=0.9' \
  -H 'cache-control: max-age=0' \
  -H 'dnt: 1' \
  -H 'sec-ch-ua: "Not_A Brand";v="8", "Chromium";v="120", "Google Chrome";v="120"' \
  -H 'sec-ch-ua-mobile: ?0' \
  -H 'sec-ch-ua-platform: "Windows"' \
  -H 'sec-fetch-dest: document' \
  -H 'sec-fetch-mode: navigate' \
  -H 'sec-fetch-site: none' \
  -H 'sec-fetch-user: ?1' \
  -H 'upgrade-insecure-requests: 1' \
  -H 'user-agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36' \
  --compressed

Response body:

{
    "AssetId": 1818,
    "Name": "Classic: Crossroads",
    "Description": "The classic ROBLOX level is back!",
    "Created": "5/1/2007",
    "Updated": "8/2/2023",
    "FavoritedCount": 214003,
    "Url": "https://www.roblox.com/games/1818/Classic-Crossroads",
    "ReportAbuseAbsoluteUrl": "https://www.roblox.com/abusereport/asset?id=1818\u0026RedirectUrl=%2fgames%2f1818%2fClassic-Crossroads",
    "IsFavoritedByUser": false,
    "IsFavoritesUnavailable": false,
    "UserCanManagePlace": false,
    "VisitedCount": 9821934,
    "MaxPlayers": 8,
    "Builder": "Roblox",
    "BuilderId": 1,
    "BuilderAbsoluteUrl": "https://www.roblox.com/users/1/profile/",
    "IsPlayable": false,
    "ReasonProhibited": "AnonymousAccessProhibited",
    "ReasonProhibitedMessage": "Guest users are not allowed.",
    "IsCopyingAllowed": true,
    "PlayButtonType": "FancyButtons",
    "AssetGenre": "Fighting",
    "AssetGenreViewModel": {
        "DisplayName": "Fighting",
        "Id": 10
    },
    "OnlineCount": 12,
    "UniverseId": 13058,
    "UniverseRootPlaceId": 1818,
    "TotalUpVotes": 52991,
    "TotalDownVotes": 7526,
    "UserVote": null,
    "OverridesDefaultAvatar": false,
    "UsePortraitMode": false,
    "Price": 0,
    "VoiceEnabled": false,
    "CameraEnabled": false
}

Response headers:

Access-Control-Allow-Credentials:
true

Cache-Control:
private, must-revalidate
Content-Encoding:
gzip
Content-Length:
579
Content-Security-Policy:
report-uri https://metrics.roblox.com/v1/csp/report?type=enforce; upgrade-insecure-requests; script-src 'self' 'unsafe-inline' roblox.com *.evidon.com *.gigya.com *.google-analytics.com *.ns1p.net adservice.google.com cdn.arkoselabs.com connect.facebook.net funcaptcha.com js.rbxcdn.com js.stripe.com long.open.weixin.qq.com midas.gtimg.cn radar.cedexis.com res.wx.qq.com roblox-api.arkoselabs.com roblox-load-generator-configuration.s3.us-east-2.amazonaws.com s.ytimg.com sb.scorecardresearch.com static.rbxcdn.com www.google.com www.gstatic.com www.youtube.com h.online-metrix.net request.eprotect.vantivcnp.com request.eprotect.vantivpostlive.com *.googletagmanager.com *.googleadservices.com googleads.g.doubleclick.net cdn.veriff.me *.lightstep.com client-api.arkoselabs.com api.arkoselabs.com; img-src 'self' data: *.cloudfront.net *.google-analytics.com *.google.com *.kaptcha.com *.rbxcdn.com *.roblox.com *.robloxlabs.com googleads.g.doubleclick.net i.ytimg.com www.googletagmanager.com robloxcorp.s.llnwi.net roblox-poc.global.ssl.fastly.net d1unuk07s6td74.cloudfront.net; connect-src 'self' *.roblox.com *.robloxlabs.com *.rbx.com *.rbxcdn.com *.roblox.cn *.simulpong.com *.lightstep.com *.ns1p.net *.arkoselabs.com *.kaptcha.com *.google.com *.google-analytics.com *.doubleclick.net *.sentry.io wss://realtime.roblox.com wss://realtime.sitetest1.robloxlabs.com wss://realtime.sitetest2.robloxlabs.com wss://realtime.sitetest3.robloxlabs.com wss://realtime-signalr.roblox.com *.braintree-api.com *.braintreegateway.com d1q2u37vreaobr.cloudfront.net funcaptcha.com robloxcorp.s.llnwi.net roblox-poc.global.ssl.fastly.net d1unuk07s6td74.cloudfront.net;
Content-Type:
application/json; charset=utf-8
Cross-Origin-Opener-Policy:
same-origin-allow-popups
Date:
Mon, 08 Jan 2024 17:22:58 GMT
Nel:
{"report_to":"network-errors","max_age":604800,"success_fraction":0.001,"failure_fraction":1}
P3p:
CP="CAO DSP COR CURa ADMa DEVa OUR IND PHY ONL UNI COM NAV INT DEM PRE"
Report-To:
{"group":"network-errors","max_age":604800,"endpoints":[{"url":"https://ncs.roblox.com/upload"}]}
Strict-Transport-Security:
max-age=31536000; includeSubdomains
Vary:
Accept-Encoding
X-Frame-Options:
SAMEORIGIN
X-Roblox-Edge:
lhr2
X-Roblox-Region:
us-central

Thumbnails and Media

To down-chive most of the media, we need to use a Universe ID. The section above shows that we can easily get a Universe ID from a place/asset ID. Doing https://games.roblox.com/v2/games/UNIVERSEID/media will give us the thumbnails and other media that is part of the universe:



curl:

curl 'https://games.roblox.com/v2/games/13058/media' \
  -H 'authority: games.roblox.com' \
  -H 'accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7' \
  -H 'accept-language: en-GB,en;q=0.9' \
  -H 'cache-control: max-age=0' \
  -H 'dnt: 1' \
  -H 'sec-ch-ua: "Not_A Brand";v="8", "Chromium";v="120", "Google Chrome";v="120"' \
  -H 'sec-ch-ua-mobile: ?0' \
  -H 'sec-ch-ua-platform: "Windows"' \
  -H 'sec-fetch-dest: document' \
  -H 'sec-fetch-mode: navigate' \
  -H 'sec-fetch-site: none' \
  -H 'sec-fetch-user: ?1' \
  -H 'upgrade-insecure-requests: 1' \
  -H 'user-agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36' \
  --compressed

Response body:

{
    "data": [
        {
            "assetTypeId": 1,
            "assetType": "Image",
            "imageId": 696217389,
            "videoHash": null,
            "videoTitle": null,
            "approved": true,
            "altText": null
        }
    ]
}

Response headers:

Cache-Control:
no-store, must-revalidate, no-cache
Content-Length:
134
Content-Type:
application/json; charset=utf-8
Date:
Mon, 08 Jan 2024 17:38:07 GMT
Nel:
{"report_to":"network-errors","max_age":604800,"success_fraction":0.001,"failure_fraction":1}
Pragma:
no-cache
Report-To:
{"group":"network-errors","max_age":604800,"endpoints":[{"url":"https://ncs.roblox.com/upload"}]}
Roblox-Machine-Id:
a43dd66b3802
Server:
Kestrel
Strict-Transport-Security:
max-age=3600
X-Frame-Options:
SAMEORIGIN
X-Roblox-Edge:
lhr2
X-Roblox-Region:
us-central

Sometimes, a YouTubeVideo asset type can appear in the results. The videoHash is the id of the YouTube video:

{
    "data": [
        {
            "assetTypeId": 33,
            "assetType": "YouTubeVideo",
            "imageId": null,
            "videoHash": "RUJHS4ZqP40",
            "videoTitle": "Plates of Fate: Remastered Trailer",
            "approved": true,
            "altText": null
        },
        {
            "assetTypeId": 1,
            "assetType": "Image",
            "imageId": 6143547736,
            "videoHash": null,
            "videoTitle": null,
            "approved": true,
            "altText": null
        },
        {
            "assetTypeId": 1,
            "assetType": "Image",
            "imageId": 7128289744,
            "videoHash": null,
            "videoTitle": null,
            "approved": true,
            "altText": null
        },
        {
            "assetTypeId": 1,
            "assetType": "Image",
            "imageId": 7128289251,
            "videoHash": null,
            "videoTitle": null,
            "approved": true,
            "altText": null
        }
    ]
}

There are thumbnail APIs for Place/Asset IDs:

These, however, are a lot more finicky than the above universe-media API.

Old Auto-Generated Thumbnail Quirks

Old place thumbnails, such as http://t4.roblox.com/Place-420x230-6cdfc833e54fbbb27029fd94c59c200c.Png use the same hash as the actual place itself. This is most likely from how their auto-generated images used to work. I have no idea if this still works or not.

Uncopylocked Places

A copylocked place cannot be down-chived. However, uncopylocked places are accessible on Roblox Studio and can be republished by anyone. If a place is uncopylocked, then we can easily archive each and every version available as a guest.

For example, Crossroads has the Asset ID “1818”. If we check the “IsCopyingAllowed” value from the metadata, we can see if Copylock was disabled for that place. By sending a post request to https://assetdelivery.roblox.com/v2/assetId/1818/version/*, we can get every version available.

Note that 0 is not the first version, but the most recent version available. This might cause some issues; 25891754, for example, gives 403 on version 0 but works as it should on version 1. The most recent version might be blocked from downloading due to it being under review by Roblox, causing the error. That’s just speculation, though. Best to check twice, just like how Santa Claus does it.

Note that when we do this, we aren’t getting all other places in the Universe. This only gets the place that we requested. If you suspect that there are other uncopylocked places in that universe you would need to find all of the places in the universe, check if each is uncopylocked and if they are down-chive them.

There is a grey area in grabbing uncopylocked places from accounts that stole popular copylocked places via hacking. But, since they are available to guest users, we should down-chive these places regardless if they are stolen.

Example:



curl:

curl 'https://assetdelivery.roblox.com/v2/assetId/1818/version/5' \
  -H 'authority: assetdelivery.roblox.com' \
  -H 'accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7' \
  -H 'accept-language: en-GB,en;q=0.9' \
  -H 'cache-control: max-age=0' \
  -H 'dnt: 1' \
  -H 'sec-ch-ua: "Not_A Brand";v="8", "Chromium";v="120", "Google Chrome";v="120"' \
  -H 'sec-ch-ua-mobile: ?0' \
  -H 'sec-ch-ua-platform: "Windows"' \
  -H 'sec-fetch-dest: document' \
  -H 'sec-fetch-mode: navigate' \
  -H 'sec-fetch-site: none' \
  -H 'sec-fetch-user: ?1' \
  -H 'upgrade-insecure-requests: 1' \
  -H 'user-agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36' \
  --compressed

Response body:

{
    "locations": [
        {
            "assetFormat": "source",
            "location": "https://c0.rbxcdn.com/693a41a996e0e9225c50683d3cd771f8"
        }
    ],
    "requestId": "638399616602945164",
    "IsHashDynamic": false,
    "IsCopyrightProtected": false,
    "isArchived": false,
    "assetTypeId": 9
}

Response headers:

Cache-Control:
no-cache
Content-Length:
227
Content-Type:
application/json; charset=utf-8
Date:
Thu, 04 Jan 2024 16:40:59 GMT
Nel:
{"report_to":"network-errors","max_age":604800,"success_fraction":0.001,"failure_fraction":1}
P3p:
CP="CAO DSP COR CURa ADMa DEVa OUR IND PHY ONL UNI COM NAV INT DEM PRE"
Report-To:
{"group":"network-errors","max_age":604800,"endpoints":[{"url":"https://ncs.roblox.com/upload"}]}
Roblox-Assetid:
1818
Roblox-Assetversionnumber:
5
Roblox-Machine-Id:
CHI2-WEB4973
Server:
Microsoft-IIS/10.0
Strict-Transport-Security:
max-age=3600
X-Frame-Options:
SAMEORIGIN
X-Powered-By:
ASP.NET
X-Roblox-Edge:
lhr2
X-Roblox-Region:
us-central

By the way, certain places can reach up to unfathomable sizes; for example, the last time I downloaded “The Scary Elevator🔪”, it was over 58 GB uncompressed (4.9 GB compressed). I even had one that was over 100 GB in size!

It is severely recommended that we put every version we get into the tracker pool as seperate items, rather than a massive place item that never gets completed.

Audio

The audio privacy update has destroyed most of the old Roblox as it sounded, but some parts may remain due to Roblox allowing for some user-uploaded sound effects to be kept public.

Do note that they’re not “lost” or “deleted”; they’re just hidden in the labyrinth maze known as rbxcdn. Someone could get a supercomputer and a million-gigabit internet and ram the servers with loads of requests, but sadly that is a waste of government money and too impractical to implement in a quick amount of time.

“Stock sounds (from APM Music) should not be downloaded, as it could cause some legal copyright issues…” is what I would say if we weren’t the Archive Team, baby! Take it all! NOTHING LEFT BEHIND! YEAH!!

Ahem. The audio link for a sound looks like this: https://create.roblox.com/marketplace/asset/12222253

https://apis.roblox.com/toolbox-service/v1/items/details?assetIds=12222253 gives us the metadata of that item:



curl:

curl 'https://apis.roblox.com/toolbox-service/v1/items/details?assetIds=12222253' \
  -H 'authority: apis.roblox.com' \
  -H 'accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7' \
  -H 'accept-language: en-GB,en;q=0.9' \
  -H 'cache-control: max-age=0' \
  -H 'dnt: 1' \
  -H 'sec-ch-ua: "Not_A Brand";v="8", "Chromium";v="120", "Google Chrome";v="120"' \
  -H 'sec-ch-ua-mobile: ?0' \
  -H 'sec-ch-ua-platform: "Windows"' \
  -H 'sec-fetch-dest: document' \
  -H 'sec-fetch-mode: navigate' \
  -H 'sec-fetch-site: none' \
  -H 'sec-fetch-user: ?1' \
  -H 'upgrade-insecure-requests: 1' \
  -H 'user-agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36' \
  --compressed

Response body:

{
    "data": [
        {
            "asset": {
                "audioDetails": {
                    "audioType": "Unknown",
                    "artist": null,
                    "title": "victory.wav",
                    "musicAlbum": null,
                    "musicGenre": null,
                    "soundEffectCategory": null,
                    "soundEffectSubcategory": null,
                    "tags": [
                        "Accordion",
                        "Jingle_(music)",
                        "Silence",
                        "Television"
                    ]
                },
                "id": 12222253,
                "name": "victory.wav",
                "typeId": 3,
                "assetSubTypes": null,
                "assetGenres": [
                    "All"
                ],
                "ageGuidelines": null,
                "isEndorsed": false,
                "description": "\\sounds\\victory.wav",
                "duration": 1,
                "createdUtc": "2009-06-24T21:16:23.903Z",
                "updatedUtc": "2022-05-03T06:54:43.293Z",
                "creatingUniverseId": null,
                "isAssetHashApproved": true,
                "visibilityStatus": null
            },
            "creator": {
                "id": 1,
                "name": "Roblox",
                "type": 1,
                "isVerifiedCreator": true,
                "latestGroupUpdaterUserId": null,
                "latestGroupUpdaterUserName": null
            },
            "voting": {
                "showVotes": false,
                "upVotes": 0,
                "downVotes": 0,
                "canVote": false,
                "userVote": null,
                "hasVoted": false,
                "voteCount": 0,
                "upVotePercent": 0
            },
            "product": {
                "productId": 2085239,
                "price": 0,
                "isForSaleOrIsPublicDomain": true
            }
        }
    ]
}

Response headers:

Content-Length:
934
Content-Type:
application/json; charset=utf-8
Date:
Thu, 04 Jan 2024 18:21:33 GMT
Nel:
{"report_to":"network-errors","max_age":604800,"success_fraction":0.001,"failure_fraction":1}
Report-To:
{"group":"network-errors","max_age":604800,"endpoints":[{"url":"https://ncs.roblox.com/upload"}]}
Server:
envoy
Strict-Transport-Security:
max-age=3600
X-Envoy-Upstream-Service-Time:
32
X-Roblox-Edge:
lhr2
X-Roblox-Region:
us-central

It should be accessible with https://assetdelivery.roblox.com/v2/assetId/12222253, but we are unauthorised to access it this way (even through a signed-in account).

However, we can send a POST request to https://assetdelivery.roblox.com/v1/assets/batch to access what we want:



curl:

curl 'https://assetdelivery.roblox.com/v1/assets/batch' \
  -H 'authority: assetdelivery.roblox.com' \
  -H 'accept;' \
  -H 'accept-language: en-GB,en;q=0.9' \
  -H 'content-type: application/json' \
  -H 'dnt: 1' \
  -H 'origin: https://create.roblox.com' \
  -H 'referer: https://create.roblox.com/' \
  -H 'roblox-browser-asset-request: true' \
  -H 'roblox-place-id: 0' \
  -H 'sec-ch-ua: "Not_A Brand";v="8", "Chromium";v="120", "Google Chrome";v="120"' \
  -H 'sec-ch-ua-mobile: ?0' \
  -H 'sec-ch-ua-platform: "Windows"' \
  -H 'sec-fetch-dest: empty' \
  -H 'sec-fetch-mode: cors' \
  -H 'sec-fetch-site: same-site' \
  -H 'user-agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36' \
  -H 'x-csrf-token;' \
  --data-raw '[{"requestId":"12222253","assetId":12222253}]' \
  --compressed

Response body:

[
  {
    "location": "https://c7.rbxcdn.com/0ef0b259dca4f3bae6c18d946c45829d",
    "requestId": "12222253",
    "IsHashDynamic": true,
    "IsCopyrightProtected": false,
    "isArchived": false
  }
]

Response headers:

Access-Control-Allow-Credentials:
true
Access-Control-Allow-Origin:
https://create.roblox.com
Cache-Control:
no-cache
Content-Length:
163
Content-Type:
application/json; charset=utf-8
Date:
Thu, 04 Jan 2024 15:44:48 GMT
Nel:
{"report_to":"network-errors","max_age":604800,"success_fraction":0.001,"failure_fraction":1}
P3p:
CP="CAO DSP COR CURa ADMa DEVa OUR IND PHY ONL UNI COM NAV INT DEM PRE"
Report-To:
{"group":"network-errors","max_age":604800,"endpoints":[{"url":"https://ncs.roblox.com/upload"}]}
Roblox-Machine-Id:
CHI2-WEB4973
Server:
Microsoft-IIS/10.0
Strict-Transport-Security:
max-age=3600
X-Frame-Options:
SAMEORIGIN
X-Powered-By:
ASP.NET
X-Roblox-Edge:
lhr2
X-Roblox-Region:
us-central

Decals and Images

This is a confusing section, so I decided to put them together to avoid issues. Decals are an instance in Roblox Studio that holds images for displaying, while Images are the actual images themselves. The Decal type contains the Image ID in a Roblox model (.rbxmx) format.

To get the decal, we can use the assetdelivery method to download the model. But then, how do we get the image ID? Is there an API? Unfortunately, no. Thankfully, the solution is quite simple.

I’ve checked, and it seems that BTRoblox gets the Image ID by opening the Roblox model and accessing the “Texture” value. How can it do that?

“.rbxmx” is actually an XML file (most, if not all files Roblox uses for models/games are XML-based, even as a compressed binary), which means we can just open the file and find the Image ID here:

<roblox xmlns:xmime="http://www.w3.org/2005/05/xmlmime" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="http://www.roblox.com/roblox.xsd" version="4">
  <External>null</External>
  <External>nil</External>
  <Item class="Decal" referent="RBX0">
    <Properties>
      <token name="Face">5</token>
      <string name="Name">Decal</string>
      <float name="Shiny">20</float>
      <float name="Specular">0</float>
      <Content name="Texture">
        <url>http://www.roblox.com/asset/?id=14889663526</url>
      </Content>
      <bool name="archivable">true</bool>
    </Properties>
  </Item>
</roblox>

After that, we can use the assetdelivery method to down-chive the Image ID! Yippee!

Wait, one question; what’s “http://www.roblox.com/asset/?id=”?

The mystery of http://www.roblox.com/asset/?id=

This URL is used for the Roblox client and studio applications. This can be shortened to “rbxassetid://”, essentially a URI scheme for the above (Do not confuse it with “rbxasset://”, which is for the local content folder in Roblox’s).

The Roblox Docs for Assets is a good read if you want to know more, but essentially Roblox only allows their software to access the “asset/?id=” API.

I tried using Fiddler, but it seems that Roblox disallows proxies from being used with their apps. (https://www.telerik.com/forums/some-apps-not-running-while-fidder-capturing)

It might be that “http://www.roblox.com/asset/?id=” is now just a redirector to “rbxassetid://”, which directs to assetdelivery, making the “asset/?id=” url useless. But there is a small chance that the software does request this URL and the server does respond to those requests. Anyone with better sniffing techniques, please test this out!

Product IDs

A Product ID is how Roblox deals with purchases, free or not. Decals, gears, catalogue items, whatever. Seems to only be used for Roblox to track sales and all that.

[WIP]

Developer Product IDs

A Developer Product ID (most definitely not to be confused with Product IDs) is a microtransaction in a Roblox place. If a Developer Product is found, we should be able to use the API to grab the metadata.

[WIP]

Fonts

There are three pages full of fonts on the Creator Marketplace. Getting a font is easy; use the assetdelivery method on the font (https://assetdelivery.roblox.com/v2/assetId/12187371840), then open up the .rbxmx file:

{
    "name": "Silkscreen",
    "faces": [
        {
            "name": "Regular",
            "weight": 400,
            "style": "normal",
            "assetId": "rbxassetid://12187311189"
        },
        {
            "name": "Bold",
            "weight": 700,
            "style": "normal",
            "assetId": "rbxassetid://12187314224"
        }
    ]
}

Then, get the assetIds and use the assetdelivery method again on those to get the actual fonts.

Update: Creator Store

Roblox released a new statement on how they will remove the ability to pay for development assets (plugins) using Robux (opting for real-world currency) and granting the ability for users to sell models on the marketplace. This was announced at Roblox’s RDC 2023 and will roll out to users in waves. Everyone will be able to purchase these items in late March.

This will cause a mass extinction in the Creator Store, as many users will decide to remove their free models from public access and make them paid access.

Response template


curl:


Response body:


Response headers: