Skip to content

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.

Published on 7 min read

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:

ExportTypical scopeNotes
Standard exportPublic channelsAvailable to workspace owners and admins. On the free plan, older history may no longer exist to export.
Corporate / full exportPublic and private channels, DMs and group DMsPaid 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 exportOrg-wide, including private channels and DMs where enabledRun 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. 1709561100 is 2024-03-04 14:05:00 UTC.
  • Identity: within a conversation, ts uniquely identifies the message. Slack's own APIs address messages by channel plus ts. 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:

MarkupMeaning
<@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:

  1. 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
    
  2. Check coverage. Compare folders against channels.json, groups.json, dms.json and mpims.json. List first and last day files per channel against the requested date range.

  3. Look for gaps. A normally busy channel with missing weeks may reflect retention, a scoped export, or deletion. Document which.

  4. Validate JSON. Every day file should parse as an array. Truncated files suggest a damaged download.

  5. Cross-check identities. Resolve user IDs through users.json, and note deleted: true accounts 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.json and org_users.json to resolve user IDs to names. It reads channels.json, groups.json, dms.json and mpims.json to label conversations as public channels, private channels, DMs (with both members) and group DMs.
  • Parses every <folder>/<YYYY-MM-DD>.json day file and uses ts (as UTC) for the timeline. It keeps the raw ts, subtype and client_msg_id in the message detail panel. Each message ID is the folder plus ts.
  • Rewrites <@U…>, <#C…|name>, <!subteam^…> and <url|label> markup into readable text that keeps the URL next to its label, and decodes HTML entities. When text is empty, it falls back to legacy attachment text. Block Kit blocks are not parsed separately.
  • Maps edited.ts to the edit time, thread_ts to a reply link when it differs from ts, and subtype: tombstone to a deleted flag. Reactions are shown as emoji name and count (reacting users are not listed). File entries are shown with name, type, size and url_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.

Related articles

Read Received chains bottom-up, compare Return-Path, From and Reply-To, interpret SPF/DKIM/DMARC results and decode MIME across EML, MBOX, MSG and PST.
Acquire Teams chats via Purview eDiscovery, the Microsoft Graph chatMessage API or a Teams (free) export, and know which fields, edits and deletions matter.
Messaging forensics explained: acquisition options, a normalized message model, the timestamp zoo, evidence hashing and the pitfalls that break timelines.