Output and exit codes
Data goes to stdout. Progress bars, confirmations, footers, warnings, prompts and errors go to stderr. So a redirect gives you clean data, and you still see what happened.
The five response shapes
Section titled “The five response shapes”Collections
Section titled “Collections”$ proton mail messages list --unread --limit 3ID FROM SUBJECT DATE FLAGS──────── ──────────────── ───────────────────── ──────────────── ─────5bH2mQxK Fastmail Billing Invoice #2291 ready 2026-04-15 14:32 ●★29xL4pQrT Trailhead Weekly digest 2026-04-15 09:02 ●2mNp7RsV Jane Roe Re: Quarterly numbers 2026-04-14 17:48
3 of 47 messages. Next page: --page 1ID is always the first column. FLAGS reads ● for unread, ★ for starred, ✗ for a message Proton flagged as phishing or suspicious, ⧗ for mail that deletes itself, and a number for attachments.
An empty collection prints nothing on stdout, so a redirect yields an empty file rather than a stray header. On stderr it says No messages., or No messages match. when a filter was applied.
Every list takes --page and --limit; --limit 0 returns the whole collection. Most also take --sort and --desc, and each names the sort keys it accepts. A listing whose order is the answer takes neither: mail comes back newest first, your addresses in the order the first one is the default, and your filters in the order they run.
Records
Section titled “Records”$ proton drive items get /Documents/report.pdfName: report.pdfLocation: /DocumentsType: fileMIME Type: application/pdfUploaded: 2026-04-02 11:20Signature: verifiedSize: 2.4 MBShared: yesID: 7Kd91mQxDocuments
Section titled “Documents”Decrypted content meant to be read: a header block, a blank line, the body, and whatever follows it. --body-only gives you the body alone.
$ proton mail messages get 5bH2mQxKSubject: Invoice #2291 is readyFrom: Fastmail Billing <billing@fastmail.com>Date: 2026-04-15 14:32Signature: verifiedID: 5bH2mQxK
Hi Roman, your invoice is attached.
AttachmentsID NAME SIZE──────── ──────────────── ───────kQ81mDx4 invoice-2291.pdf 84.2 KBMutations
Section titled “Mutations”$ proton mail messages trash --unread --older-than 30d✓ Moved 12 messages to trash.When something is created, its ID goes to stdout and the confirmation to stderr, so capturing it is a plain assignment:
LABEL=$(proton mail settings labels create --name Work)Streams
Section titled “Streams”mail messages watch and calendar reminders watch stay attached and print a line the moment something happens.
$ proton mail messages watch14:32 5bH2mQxK Fastmail Billing Invoice #2291 ready14:41 9xL4pQrT Trailhead Weekly digestIn JSON, each line is one object, which jq reads without --slurp. There is no envelope and no footer.
Ctrl+C or SIGTERM ends a watch quietly with exit 0, so a service manager does not log a failure every time you stop it.
Errors
Section titled “Errors”One line for the problem, then an indented Try: block for the fix.
$ proton mail messages get 5bH2mQxK --render htmError: --render accepts: text, html, raw.Anything judgeable from your command line alone is caught before signing in or contacting Proton: a misspelled setting key, an impossible --render, a colour outside Proton’s palette.
Exit codes
Section titled “Exit codes”The message says what happened and what to try; the code is what a script reads.
| Code | Meaning | What to do |
|---|---|---|
0 |
Success | |
1 |
Something you passed was wrong | Fix the command. Nothing was sent |
2 |
Authentication failed | Sign in again, or fix the password |
3 |
Not found | Run the matching list |
4 |
Ambiguous, or a conflict | Narrow the term, or use the ID it printed |
5 |
Network or server problem, including a rate limit | Wait and retry |
6 |
Refused by your confirmation policy | Change the policy; nothing about the command was wrong |
7 |
A bug in proton | Report it. Nothing you typed caused it |
8 |
Not supported yet | See What it can’t do |
130 |
Cancelled with Ctrl+C |
For a scheduled job the difference between 2 and 5 is the one that matters: 2 means fix the credential, 5 means come back later. Retrying never helps with 6 or 8.
JSON and YAML
Section titled “JSON and YAML”proton mail messages list --output jsonproton mail messages list --output yamlEvery collection is one envelope, keyed by its plural name:
{ "messages": [ { "id": "5bH2…", "subject": "Invoice #2291 is ready", "unread": true } ], "count": 3, "total": 47, "page": 0, "page_size": 3, "has_more": true}count is always present. total, page, page_size and has_more appear when the request involved them, so a consumer can tell “page 0” from “not paginated”.
A list is always a list. An empty collection is [] and an empty map is {}, never a missing key. So jq '.attendees[]' iterates nothing rather than failing on null.
A scalar that does not apply is left out entirely. For example, occurrence_count appears only when the number is known. An event that does not recur has no count, and neither does a series with no end date. Read rrule to tell those two apart.
Names, not numbers, matching the text output and set:
{ "type": "file", "state": "active", "unread": true }Keys are snake_case, the collection’s own key included: a listing of watched addresses is watched_addresses. Timestamps are <verb>_time in Unix seconds. Sizes are size in bytes. IDs are always complete.
Times come back in the zone you are working in - --zone, or TZ, or zone: in your config, or your system’s. An event’s start and end are RFC 3339 with that offset, and the zone the event is anchored to is its own field:
{ "start": "2026-04-16T16:00:00+02:00", "end": "2026-04-16T17:00:00+02:00", "zone": "Europe/Vienna" }An all-day event has "all_day": true. It begins at midnight on the date it names and ends at the midnight after its last day, so end - start is how long it lasts.
Mutations emit JSON too:
$ proton mail settings labels create --name Work --output json{ "action": "created", "count": 1, "dry_run": false, "ids": ["kQ81mDx4T9…"], "kind": "label", "name": "Work" }The one exception is proton api, which passes Proton’s response through unchanged.
Colour
Section titled “Colour”Colour marks the parts that carry a verdict: ✓ green, ! yellow, Error: red, ✗ red, IDs magenta, ● unread, ★ starred, and the signature, DMARC and Flagged lines green, yellow or red. A ■ beside a label, folder, calendar or group is the exact colour Proton stores for it. Everything else stays plain.
Shades come from your terminal’s theme, not from proton.
Colour is off whenever output is piped or redirected, under --output json or yaml, and with --no-color or NO_COLOR. It never changes the layout, and it never carries meaning on its own: every verdict is spelled out in words.
--force-color or FORCE_COLOR paints anyway, for the cases proton cannot see: a pager, a multiplexer, a CI log that renders the escapes itself.
proton mail messages list --force-color | less -RFORCE_COLOR=1 proton mail messages list | tee run.logOn a terminal, a table too wide to fit gives up room from its widest flexible column, never from a date or an ID. Piped output is never truncated.
Widths are measured in terminal cells, so a subject in Japanese or a filename with an emoji stays aligned.
--quiet silences confirmations, notes, footers and progress. It never silences the answer.
Prompts
Section titled “Prompts”Only account login and the commands that need your password again ever ask a question, and only when standard input is a terminal. Everything else fails with a message rather than waiting, so a scheduled job never hangs. Prompts go to stderr.
proton account login --no-input # fail instead of askingPROTON_NO_INPUT=1 proton account login