references/export-formats.md — Chat Export to Transcript
From version 0.1.1 of Chat Export to Transcript, shown publicly by its creator. 7.9 KiB · SHA-256 d0df1761160800978ea983e608a58f0698664c6a3365b9290c096473c980897d.
# Official export formats and transcript reference
## Accepted inputs
`INPUT` is a local, user-provided official export. `--format` can explicitly
select `whatsapp`, `discord`, `telegram`, or `slack` if detection is ambiguous.
The converter does not read application caches, local databases, sessions, or
tokens, and it makes no network request.
| Format | Accepted layout | Notes |
| --- | --- | --- |
| WhatsApp | Export chat `.txt` or its `.zip` containing that file | Common locale date and time styles, multiline messages, system events, media omission notices, and attachment names. |
| Discord | Request all of my data package with `messages/<channel>/messages.json` or `messages.csv` and `channel.json` | Channels are grouped by channel ID from channel.json, falling back to the unique channel folder name when the ID is absent; display names never determine grouping. |
| Telegram Desktop | Export chat history `result.json` | Choose JSON when exporting; HTML is unsupported. |
| Slack | Workspace export with `users.json` and `<channel>/<date>.json` folders at the root next to `channels.json`; legacy `channels/<channel>/<date>.json` is also accepted | User IDs can be resolved to display names from `users.json`; channels.json is not read. |
The tool reports unparseable lines or records as skipped rather than inventing
messages. A format mismatch or unsupported layout should be corrected by
choosing an official export and, when useful, the matching `--format`.
In a WhatsApp text export, a line inside a message body that matches the
date-and-time header pattern starts a new record if its timestamp parses. An
invalid header timestamp is counted as skipped, and continuation lines after
that invalid header are also skipped until the next valid header. The text format cannot
distinguish that line from an actual message header. Discord's official data
package may omit a per-message author; those messages show `Unknown` as the
author in JSONL and transcripts.
## Uniform JSONL
`OUT/messages.jsonl` contains one JSON object per message. The fields are
`platform`, `conversation`, `message_id`, `timestamp` in ISO 8601 form with
an offset when the source provides one, `author`, `text`, `attachments`,
`reply_to`, and a Boolean `system` flag. All nine fields are present. Unknown
message IDs and reply targets are JSON `null`; `attachments` is a list of
strings. A record without a parseable timestamp is skipped rather than given
an invented time. WhatsApp timestamps with no timezone in the export remain
offset-free; Slack timestamps use UTC. Attachment references are metadata;
the converter does not fetch or embed media.
## Readable transcripts
For Discord, `conversation` is the channel ID or, if absent, the channel folder
name. Other formats retain their conversation label.
Each conversation has one text file under `OUT/transcripts/`, named from a
sanitized conversation label followed by the first eight characters of its
SHA-256 hash. The hash distinguishes conversations whose labels sanitize to
the same filename. A message begins
with one header and zero or more continuation lines:
```text
2025-01-02T03:04:05+00:00 Example Sender: First line
| Second line
```
The header shape is `TIMESTAMP NAME: first line`. Each later line starts
with four spaces and `| `. Control characters, tabs, U+2028, and U+2029 in
untrusted text are rendered as visible `\uXXXX` or `\UXXXXXXXX` escapes;
line breaks that separate message content become continuation lines. Names,
attachment names, and human-readable reports are escaped to one line so
export content cannot impersonate another header, path, or approval line.
JSONL uses normal JSON escaping.
## Approval and completeness
`convert INPUT --out DIR --dry-run` reads the source, prints every exact
planned file and an approval digest, and writes nothing. The destination must
be new or empty. Live conversion requires `--approve` with the digest of the
fresh plan. Every output file is created only; an existing path or symlink is
refused. Re-run the dry run if the source or destination changes.
Skipped malformed lines or records make the result `incomplete`. The command
lists the skipped items and exits 2 unless `--allow-partial` is supplied;
that flag changes the exit status, not the incomplete marker. An official
export contains only what its provider included at export time and may omit
messages due to provider policy or the user's export choices.
WhatsApp (Meta), Discord, Telegram, and Slack (Salesforce) are trademarks of
their owners. This product is independent and unaffiliated.
## Local safety requirements
Supported platforms are macOS and Linux with Python 3.10 or newer and POSIX
no-follow directory descriptor APIs. Windows and other platforms are refused
before input or output I/O. Paths with symlink components, `..`, or NUL are refused; explicit absolute user paths are allowed. Percent signs and backslashes are literal local filename characters; paths are never URL-decoded. ZIP member names still refuse percent signs and backslashes.
On macOS use the physical path (for example `/private/tmp`, not its alias).
Every read file is limited to 33,554,432 bytes (32 MiB), including the selected
ZIP and its chat TXT member; ZIPs may contain at most 100 members. There is no
stdin input. Every ZIP member name is validated before reading member content;
ZIP members are never extracted. Directory exports enumerate all immediate
channel folders and read all supported message files, plus Discord channel
metadata or Slack users.json; no conversation filter reduces these reads.
Attachment names and reply IDs are retained as metadata, never opened or fetched.
Approval binds the fully resolved destination, its existing directory identity
(device and inode), its parent identity, directory creation, and every output
file's bytes. The digest covers rendered output, not ignored source fields.
The preview lists creation of the destination and transcripts directory. The
immediate output parent must already exist: no ancestors are created. Live
writes walk directories without following symlinks, verify approved identities,
and create files exclusively relative to directory descriptors. Write failures
retain partial output and report an error; choose a fresh output for a retry.
Malformed JSON, ZIPs, nested metadata and attachment shapes fail with an explicit
error. Invalid message records are listed as skipped with incomplete status;
`--allow-partial` explicitly accepts those omissions. Header-shaped WhatsApp
body lines remain ambiguous. This tool does not authenticate source messages,
verify provider export completeness, or verify reply targets; reply IDs are
copied without correlation checks. There is no separate verification command.
Use the physical path when an ancestor is a symlink (for example /private/tmp on macOS).
A symlink anywhere inside an export directory is refused; directory metadata is checked throughout Discord and Slack exports.
Discord transcripts begin with the validated readable channel.json name (or the grouping key if absent), followed by the channel ID; filenames and JSONL conversation keys remain ID-based, falling back to the folder name when the ID is absent. JSONL also includes channel_name (null when absent). Duplicate grouping keys are refused with an error naming both folders.
In Slack root layout, every root subfolder is treated as a channel folder. Its JSON files are parsed even in unrelated folders; invalid JSON fails with exit 2, and invalid records or non-array JSON mark the run incomplete (exit 2 unless --allow-partial). Non-JSON files are ignored.
Discord and Telegram timestamps use the same explicit parser on all supported Python versions: calendar dates YYYY-MM-DD or YYYYMMDD, optionally followed by T or a space and HH:MM or HHMM with optional seconds, arbitrary decimal fractions (dot or comma), and Z or a numeric HH:MM/HHMM offset. Output has whole seconds; fractions are truncated. Other forms and invalid dates are skipped and counted.
By Zero Width Lab (zerowidthlab.com)
License: MIT (read it).
MIT
MIT LicenseCopyright (c) 2026 Zero Width Lab
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.