Slack Export Forensics: ZIP Layout, ts and Tombstones
What a Slack workspace export contains, how to read ts, thread_ts, edits, tombstones and message markup, what exports leave out, and how to verify integrity.
A Slack workspace export looks simple: a ZIP of JSON files. Getting defensible answers out of it means knowing which export you have, how Slack identifies and timestamps messages, and what the export quietly leaves out. This guide covers all three and ends with an integrity checklist.
Export types and who can run them
Slack offers different export scopes depending on plan and approval. The details have changed several times, so confirm against Slack's current help center before you commit to a collection plan. Broadly:
| Export | Typical scope | Notes |
|---|---|---|
| Standard export | Public channels | Available to workspace owners and admins. On the free plan, older history may no longer exist to export. |
| Corporate / full export | Public and private channels, DMs and group DMs | Paid plans only, and has usually required applying to Slack and meeting legal conditions (for example a valid legal process, employee consent, or an applicable law). |
| Enterprise Grid org export | Org-wide, including private channels and DMs where enabled | Run by org owners, often via eDiscovery or DLP integrations as well as the export tool. |
Record which export type you received, who ran it, the date range selected, and when. That context decides which gaps are expected and which are suspicious. The export is generated in Workspace settings → Import/Export Data → Export, and Slack sends a download link when it is ready.
ZIP layout
export.zip
├── users.json # workspace members (IDs, names, profile, deleted/bot flags)
├── channels.json # public channels (ID, name, creator, members, topic, purpose)
├── groups.json # private channels (full exports)
├── dms.json # 1:1 DMs: ID + two member IDs (full exports)
├── mpims.json # group DMs: name like mpdm-alice--bob--carol-1 (full exports)
├── integration_logs.json
├── general/
│ ├── 2024-03-04.json
│ └── 2024-03-05.json
├── finance-ops/
│ └── 2024-03-06.json
└── D0123ABCD/ # DM folders are named by conversation ID
└── 2024-03-04.json
Each conversation folder holds one JSON array per day with activity. Grid exports can also include an org_users.json. Exact file sets vary by export type and age. Some exports also include files for canvases or other newer features. Inventory the ZIP before assuming anything is missing.
Day files are a storage convenience, not a statement about local days. Do not assume the file boundary equals midnight in the custodian's time zone. Order by ts.
Message anatomy
{
"type": "message",
"user": "U01AAAA",
"text": "<@U02BBBB> new bank details for INV-2291: <https://portal.example.com/inv|portal>",
"ts": "1709561100.000100",
"client_msg_id": "4f1c2e3a-0000-4000-8000-000000000001",
"thread_ts": "1709561100.000100",
"reply_count": 2,
"edited": { "user": "U01AAAA", "ts": "1709561190.000000" },
"reactions": [{ "name": "eyes", "users": ["U02BBBB"], "count": 1 }],
"files": [{ "id": "F01CCCC", "name": "iban.pdf", "mimetype": "application/pdf",
"size": 48213, "url_private": "https://files.slack.com/files-pri/T0/F01CCCC/iban.pdf" }]
}
ts: identifier and timestamp in one
ts is a string of Unix seconds with a fractional suffix (1709561100.000100). It serves two purposes:
- Time: the integer part is the post time in UTC.
1709561100is 2024-03-04 14:05:00 UTC. - Identity: within a conversation,
tsuniquely identifies the message. Slack's own APIs address messages by channel plusts. It is not globally unique, so always pair it with the channel ID.
Keep ts as a string. Converting it to a float can lose the suffix and collapse distinct messages.
Threads
A thread parent has thread_ts equal to its own ts, plus reply metadata (reply_count and, in many exports, replies or reply_users). Replies carry the parent's thread_ts and a different ts. Replies sent "also to the channel" have subtype thread_broadcast. Replies can land in day files after the parent, so rebuild threads across the whole export, not per file.
Edits
edited.ts records when the last edit happened, and edited.user who made it. The export holds only the current text. Earlier versions are not included. An edit timestamp long after the original post, on a message about payments or approvals, is worth an interview question.
Subtypes
subtype marks non-standard messages, such as channel_join, channel_leave, channel_topic, channel_purpose, bot_message, me_message, thread_broadcast and tombstone. Bot and integration posts may have a bot_id and username instead of a user, and sometimes put their content in legacy attachments or blocks rather than text.
Markup in text
Slack stores mentions and links in angle-bracket markup, with &, < and > HTML-escaped:
| Markup | Meaning |
|---|---|
<@U02BBBB> | User mention, resolved through users.json |
<#C01GEN|general> | Channel link with label |
<!here>, <!channel> | Broadcast mentions |
<!subteam^S0123|@finance> | User group mention |
<https://portal.example.com/inv|portal> | Link shown as "portal" pointing to the URL |
The label-vs-URL split is where link deception hides, just as with HTML email. Always review the target, not just the label.
Files and reactions
files entries carry metadata (name, mimetype, size, url_private), not file content. url_private requires an authenticated Slack session. Some exports have included tokenized download links, so treat the whole export as sensitive and never click through from a review workstation without authorization. Collect file content separately if it matters. reactions list each emoji name, the reacting user IDs and a count.
Tombstones and deletions
When a thread parent is deleted while replies still exist, Slack keeps a placeholder so the thread survives. That placeholder is a tombstone: subtype tombstone, with text along the lines of "This message was deleted." Tombstones prove a message existed at that ts and was removed, but not what it said.
Ordinary deleted messages, with no thread attached, are simply absent from the export. There is no record they existed. Retention policies behave the same way. If you need proof of deletion activity, look to audit logs (on plans that provide them), eDiscovery or DLP integrations, or the other parties' copies.
What exports do not include
- Previous versions of edited messages.
- Deleted messages (other than tombstones) and anything removed by retention before the export.
- File binaries. You get references and metadata only.
- Huddle and call audio or video.
- Private channels and DMs, in a Standard export.
- Complete content from Slack Connect channels shared with other organizations. Coverage depends on which side exports and on plan, so verify rather than assume.
- Workspace access logs and IP data. Those come from admin tools or audit-log APIs, not the message export.
Verifying integrity
Slack does not sign exports, so integrity is established by your own handling:
-
Hash on receipt. Hash the ZIP as downloaded, before unzipping, and record the download time and the account that ran the export.
shasum -a 256 example-corp-export.zip unzip -l example-corp-export.zip | tail -1 # member count and total size -
Check coverage. Compare folders against
channels.json,groups.json,dms.jsonandmpims.json. List first and last day files per channel against the requested date range. -
Look for gaps. A normally busy channel with missing weeks may reflect retention, a scoped export, or deletion. Document which.
-
Validate JSON. Every day file should parse as an array. Truncated files suggest a damaged download.
-
Cross-check identities. Resolve user IDs through
users.json, and notedeleted: trueaccounts and bots. Display names can change, so user IDs are the stable identifier.
Doing this in MessagingForensics
MessagingForensics reads the export ZIP directly in the browser. Drop the ZIP (or the unzipped folder), and it is SHA-256 hashed and expanded in memory without being uploaded. It then:
- Loads
users.jsonandorg_users.jsonto resolve user IDs to names. It readschannels.json,groups.json,dms.jsonandmpims.jsonto label conversations as public channels, private channels, DMs (with both members) and group DMs. - Parses every
<folder>/<YYYY-MM-DD>.jsonday file and usests(as UTC) for the timeline. It keeps the rawts,subtypeandclient_msg_idin the message detail panel. Each message ID is the folder plusts. - Rewrites
<@U…>,<#C…|name>,<!subteam^…>and<url|label>markup into readable text that keeps the URL next to its label, and decodes HTML entities. Whentextis empty, it falls back to legacy attachment text. Block Kitblocksare not parsed separately. - Maps
edited.tsto the edit time,thread_tsto a reply link when it differs fromts, andsubtype: tombstoneto a deleted flag. Reactions are shown as emoji name and count (reacting users are not listed). File entries are shown with name, type, size andurl_private. - Also accepts a single loose day file, labeled by its parent folder name.
Use the deleted-only filter to list tombstones, switch between UTC and local time, run a keyword watchlist, and export CSV, JSON or an HTML report. Open the analyzer to load an export. Related guides: Microsoft Teams forensics, email header forensics, Discord data package forensics, and what is messaging forensics.