TOMLing my Discord servers
I am in far too many Discord servers. I wanted to organize the folders they’re grouped in. This is really annoying to do in Discord itself, because it only lets you drag and drop them around one at a time in the left sidebar. So I did a little bit of messing around in Firefox’s Developer Tools to extract and reinsert my folder data.
I’m not sure if I saved time doing all this. It would have been faster to move 80 little circles around, if a bit mind-numbing. On the plus side, I learned a thing or two about Discord’s internals, and I can play around with various ways to group my servers now to see which one I like best: all I have to do is re-run a script.
Another thought I had is that it’s nice to show a bit of the behind-the-scenes thinking for this kind of “soft reverse-engineering” task. So I’m blogging about it!
I’ll call Discord servers guilds for the rest of this post, because that’s what they are called internally.
Want to play along at home? Open Discord in your browser. Open the Developer Tools (F12) and refresh. Follow the instructions in the boxes like this one. You’ll need the Python package manager uv, which is excellent at running Python scripts with dependencies. (It installs them on the fly when you run uv --with blah.)
Getting our bearings
I figured Discord must be sending some request to an API somewhere when I rearrange guilds in the sidebar. I know Discord uses WebSockets, in general, so that’s were I looked first. I tried rearranging guilds in my sidebar and seeing if a message fires on the socket. But nothing happened. Mysterious!
Then I noticed this helpful pair of lines in the JavaScript console logs:
[discord_protos.discord_users.v1.PreloadedUserSettings] Updating guildFolders with delay 10
[discord_protos.discord_users.v1.PreloadedUserSettings] Scheduling save from markDirty
Aha, “scheduling save” and “delay 10”! So maybe it’s debounced and only saves the new order after 10 seconds? I counted to 10 after rearranging my guilds, and still didn’t see anything relevant on the socket. But then I noticed it makes an HTTP PATCH request, namely to:
https://discord.com/api/v9/users/@me/settings-proto/1
The request body is, at first blush, impenetrable: {"settings": "cqIJ..."} with some base-64-encoded binary data. I figured proto probably means Protocol Buffers, which means I can’t decode the request body without a schema. Rats!
At this point I just searched for “settings-proto discord” online and found unofficial docs for this internal API. So handy! They even linked to a Python package for parsing these protos, wow. Their examples will make parsing this Protocol Buffer data easy.
Let’s save the request, hoping it contains all information about the new order (spoiler: it does).
Downloading guild folder data
In the Network tab, filter requests for @me/. Rearrange any two guilds in the sidebar and wait 10 seconds. A PATCH request should appear:
Click it, then click “Request” on the right. Right-click and “Copy All”:
Save this as folder.json.
At this point I started playing around with decoding this data, loosely following the example from the discord-protos Python package.
$ uv run --with discord-protos python3 Python 3.11.14 (main, Oct 14 2025, 21:26:53) [Clang 20.1.4 ] on linux Type "help", "copyright", "credits" or "license" for more information. >>> import json >>> j = json.loads(open("folders.json").read()) >>> j['settings'][:20] # it's base64 data 'cu8JCgoKCAAwQD/WmpAB' >>> from google.protobuf.json_format import MessageToDict >>> from discord_protos import PreloadedUserSettings >>> from base64 import b64decode >>> b = b64decode(j['settings']) >>> MessageToDict(PreloadedUserSettings.FromString(b)) {'guildFolders': {'folders': [{'guildIds': ['112760235659112448']}, ...
Alright! But a list of numeric IDs will be hard to edit. Can I get all the names corresponding to these guild IDs somehow?
Obtaining guild names
I wasn’t totally sure where to start looking for this, and briefly tried to extract this information from the page using JavaScript in Developer Tools. But then I thought to check the unofficial docs I had just found.
Indeed, there is a /@me/guilds endpoint, documented here. I could just send a request there and get a JSON object with all the information I need. Yay!
“Edit and Resend” in the Developer Tools is really handy here. I used to always “copy as cURL” and then mess with the huge cURL command before sending it off, but this is, I think, nicer.
Procedure
In the Network tab, filter requests for @me/ and find any “GET” request:
Right-click this request, choose “Edit and Resend”, and change everything after @me/ to guilds:
On the right, under the “Response” tab, you will see a big JSON response, with info about all the guilds you’re in. Right-click it and “Copy All”:
Paste this into a text editor and save it as guilds.json somewhere.
Turning folders into an easily editable text file
From here on it was just a matter of writing some useful scripts. I think the nicest JSON-like to edit by hand is TOML, so I figured I’d transform folders.json into a TOML file that has my guild names in the comments. Something like this:
guildPositions = [
# This stuff is marked as "deprecated" in the docs I found.
# I guess I won't touch it.
"...",
"...",
"...",
]
[[folders]]
guildIds = [
"756829356155731988", # ⛳ Code Golf
"1285973048092000388", # Byte Heist
]
id = "1234512345"
name = "codegolf"
[[folders]]
guildIds = [
"1208593165213245510", # ma mun
"1337041613096222730", # toki pona en toki Netelan
"861015480197447710", # toki wile
]
id = "3652584801"
name = "tp"
Did you know Python has a built-in TOML reader but not a writer? Funny. I used Tomli-W to write TOML.
Extract TOML from base64 JSON folder update
Save the following Python script as extract.py next to your .json files:
from discord_protos import PreloadedUserSettings
from base64 import b64decode
from google.protobuf.json_format import MessageToDict
import tomli_w, json, re
with open("folders.json") as f: raw_folders = json.load(f)
with open("guilds.json") as f: guilds = json.load(f)
id_to_name = {g["id"]: g["name"] for g in guilds}
b = b64decode(raw_folders["settings"])
folders = MessageToDict(PreloadedUserSettings.FromString(b))
dump = tomli_w.dumps(folders["guildFolders"])
f = lambda m: m[0].ljust(23) + "# " + id_to_name.get(m[1], "")
print(re.sub(r'"(\d{5,})",', f, dump))
Now open a command line in this folder and run
uv run --with discord-protos,tomli-w python3 extract.py > folders.toml
I think you can make up numbers for the id fields if you make new folders. Oh, and just leave the “guildPositions” section at the top alone. I think it does nothing.
At this point, I edited the TOML file to test my approach: first, just merging all my existing folders into one big “other” folder. Which means I now had to write the other script…
Applying changes
Save this Python script as reimport.py next to your .json files:
from discord_protos import PreloadedUserSettings
from base64 import b64decode, b64encode
from google.protobuf.json_format import MessageToDict, ParseDict
import tomllib, json
with open("folders.toml", "rb") as f: folders_dict = tomllib.load(f)
folders = ParseDict({'guildFolders': folders_dict}, PreloadedUserSettings())
b64 = b64encode(folders.SerializeToString()).decode('ascii')
print(json.dumps({"settings": b64}))
Now run this in the command line:
uv run --with discord-protos python3 reimport.py
It will spit out something like {"settings": "cqIJCgoKCA..."} — copy all this (you may need Ctrl+Shift+C to copy from the command line).
Now go back to the “PATCH” request in your browser. Right click, choose “Edit and Resend”, then select the stuff at the bottom and paste to replace it:
It works! So now I’ll just edit folders.toml to suit my needs and re-run reinsert.py. I’d show off the results, but I feel like they are a bit too private.