Skip to content

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.

Published on 7 min read

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:

FieldMeaning
idMessage snowflake
timestampCreation time, written with a UTC offset
timestampEditedLast edit time, or null
authorid, name, nickname, isBot, …
contentMessage text
attachmentsurl, fileName, fileSizeBytes
embedsLink previews and rich embeds
reactionsEmoji and counts
referenceThe 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.json or messages.csv, names each conversation from index.json and channel.json (as Server › #channel for server channels), and attributes every message to the owner from Account/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:

Related articles

How to acquire macOS chat.db and iOS sms.db, read the message tables, convert Cocoa timestamps, and recover text, edits, unsends and tapbacks.
Messaging forensics explained: acquisition options, a normalized message model, the timestamp zoo, evidence hashing and the pitfalls that break timelines.
Where WhatsApp evidence lives on Android and iOS, how chat exports, msgstore.db and ChatStorage.sqlite differ, and how to read their timestamps correctly.