Discord forensics: data packages and snowflake IDs
What the Discord data package contains, why it only holds the owner's messages, how to decode snowflake IDs, and when to use DiscordChatExporter output.
Discord investigations usually start from one of two artifacts: the official data package a user requests from Discord, or a channel export made with a third-party tool. They answer different questions. The data package is authoritative but one-sided; a channel export shows the whole conversation but is only as trustworthy as the account and process that produced it. This guide covers both, plus the snowflake IDs that let you recover time from almost anything Discord emits.
Requesting the data package
The account owner requests the package from User Settings → Data & Privacy → Request all of my data. Recent versions of the dialog let the user choose which categories to include; for messaging work, make sure Messages is selected. Discord then prepares the archive and emails a download link. Preparation can take days, and Discord states it may take up to 30 days. The link expires after a while, so download promptly and hash the ZIP as soon as it arrives.
Because only the account holder can request it, the package usually comes from a cooperating user, a client, or legal process directed at that user. Discord handles law enforcement requests through a separate channel, and those returns do not necessarily share this layout.
What is inside
Folder names have changed over the years (older packages used lowercase names), but the message-related structure has been stable:
package.zip
├── Account/
│ └── user.json id, username, global_name, email, …
├── Messages/
│ ├── index.json { "<channel id>": "<channel label>" }
│ ├── c1187654321098765432/
│ │ ├── channel.json id, type, name, guild, recipients
│ │ └── messages.json newer packages
│ └── c1187650000000000000/
│ ├── channel.json
│ └── messages.csv older packages
├── Servers/
└── Activity/ analytics events, if included
index.json
A flat map of channel IDs to human-readable labels, for example "Direct Message with alice" or a channel name. Some entries are simply "None", typically for channels the package cannot name anymore. Do not treat a missing label as evidence the channel was deleted.
channel.json
Describes one channel: its id, type, a name for server channels, a guild object with the server id and name, and for direct messages a recipients array of user IDs. The recipients list is often the only way to identify the other party in a DM, because the package does not include their messages or profile.
messages.json and messages.csv
Each row is one message the account owner sent:
[
{
"ID": 1187654321098765432,
"Timestamp": "2023-12-22 07:14:15",
"Contents": "Sending the file now",
"Attachments": "https://cdn.discordapp.com/attachments/1187650000000000000/1187654321098765433/report.pdf"
}
]
The CSV variant has the same four columns: ID, Timestamp, Contents, Attachments. Several attachments are separated by spaces in one cell. Exact timestamp formatting varies between package generations, so record how it appears in yours before you convert it.
Attachment URLs point to Discord's CDN. Discord now signs these URLs with expiry parameters, so a link in a package can stop working after a while. If attachment content matters, retrieve it promptly and document when and how you did it.
Account/user.json
The owner's user ID, username, display name (global_name) and account details. The user ID is itself a snowflake, so it tells you when the account was created (see below).
The limitation that matters most
The data package contains only messages sent by the account owner. Replies from other people, messages in DMs from the other side, and server messages by other members are not included. What you get is one side of every conversation, with no reliable way to reconstruct what the owner was replying to.
This shapes the findings you can write. "The owner sent X at time T in channel C" is supported. "The owner did not respond" or "nobody mentioned Y" is not, because the package never contained anyone else's words. Deleted messages are also absent: a message the owner deleted before the request does not appear.
Snowflake IDs encode time
Discord IDs for messages, users, channels, servers and attachments are 64-bit "snowflakes". The top 42 bits are milliseconds since the Discord epoch, 2015-01-01 00:00:00 UTC (Unix ms 1420070400000):
unix_ms = (id >> 22) + 1420070400000
Worked example with the message ID above:
1187654321098765432 >> 22 = 283158855700
283158855700 + 1420070400000 = 1703229255700
1703229255700 ms = 2023-12-22 07:14:15.700 UTC
The lower 22 bits hold a worker ID, a process ID and a per-process counter. They matter for uniqueness, not for timing. In a shell or a quick script:
const id = 1187654321098765432n; // use BigInt: the value exceeds 2^53
new Date(Number((id >> 22n) + 1420070400000n)).toISOString();
Use BigInt or string arithmetic. Parsing a snowflake as a JavaScript number or into a spreadsheet silently rounds the last digits, and that includes JSON files opened in tools that do not handle big integers.
Practical uses:
- Cross-check
Timestamp. The snowflake gives millisecond precision in UTC regardless of how the export formatted the time column. - Date things that have no timestamp field, such as channel IDs (channel creation), user IDs in
recipients(account creation of the other party), server IDs, and attachment IDs embedded in CDN URLs. - Detect tampering. A row whose snowflake time and timestamp disagree by more than rounding deserves attention.
The analyzer shows the message ID and channel ID on each Discord message but does not decode snowflakes for you, so use the formula above when you need the derived time.
Full channel history: DiscordChatExporter
When you need both sides of a conversation, the usual source is DiscordChatExporter, an open-source tool that downloads channel history through Discord's API and writes HTML, TXT, CSV or JSON. It needs a token: a bot token for servers where a bot is present, or a user token. Using a user token to automate an account is against Discord's Terms of Service, and whether that affects admissibility or the account depends on your jurisdiction and engagement terms. Document which method was used, by whom, and when.
The JSON output carries far more than the data package. The top level holds guild, channel, dateRange, exportedAt and messages, and each message includes:
| Field | Meaning |
|---|---|
id | Message snowflake |
timestamp | Creation time, written with a UTC offset |
timestampEdited | Last edit time, or null |
author | id, name, nickname, isBot, … |
content | Message text |
attachments | url, fileName, fileSizeBytes |
embeds | Link previews and rich embeds |
reactions | Emoji and counts |
reference | The replied-to messageId (and channel/guild IDs) |
The CSV export is flatter: AuthorID, Author, Date, Content, Attachments, Reactions. Prefer JSON when you have a choice, since CSV drops edit times and reply links.
An exporter only sees what exists on the server at export time: deleted messages are gone, and edits show the latest version with an edit time but not the previous text. It also sees only channels the token's account can read.
The desktop client cache
The Discord desktop client is a Chromium-based app, and its cache (%APPDATA%\discord\Cache\Cache_Data on Windows, with equivalents on macOS and Linux) can hold API responses, avatars and attachments, sometimes including content later deleted server-side. The analyzer reads the Chromium cache directly: drop the Cache_Data folder and it parses both the simple-cache (<hash>_0 files) and blockfile (data_0–data_3, f_*) layouts, decompresses gzip, deflate and Brotli bodies, and records each response's request and response times. Cached /channels/<id>/messages responses are turned into Discord messages (author, content, timestamp, edits, attachments, replies) with the cache URL and time kept on each row. Everything else in the cache, such as avatars and other API calls, is listed under the Artifacts tab. A cached response only shows what the client fetched at that moment, so treat it as a snapshot, not a full history.
What the analyzer does with Discord evidence
- Data package: drop the ZIP as-is. It finds
Messages/c<id>/messages.jsonormessages.csv, names each conversation fromindex.jsonandchannel.json(asServer › #channelfor server channels), and attributes every message to the owner fromAccount/user.json. Attachment URLs become attachment entries, and a warning reminds you that only the owner's messages are present. - DiscordChatExporter JSON: author, author ID, timestamp, edit time, reply reference, reactions, attachments (name, size, URL) and embed titles and URLs are mapped; bot authors are marked.
- DiscordChatExporter CSV: author, author ID, date, content, attachments and reactions; the conversation name comes from the file name.
All timestamps are normalized to UTC, and you can switch the display to your local zone.
Try it
Everything runs locally in the browser; the package never leaves your machine. Open the analyzer and drop the data package ZIP or an exporter JSON file.
Related guides:
- Slack export forensics for workspace-level exports with both sides of every conversation
- Microsoft Teams forensics
- WhatsApp forensics
- What is messaging forensics?