Output and scripting
How the CLI chooses between tables and JSON, NDJSON for streams, --json, --fields and --all, errors on stderr, exit codes, color and CI behavior, and patterns for shell scripts and agents with jq.
The CLI picks its output once per run, from where stdout goes:
| stdout | Output |
|---|---|
| A terminal | Tables for lists (with a page footer), a detail pane for one object, a success line for sends and deletes. Spinners and prompts draw on stderr. |
A pipe or a file, or with --json | One JSON document per command: the API response, untouched apart from --fields. Streams (tail, events stream) print NDJSON, one JSON object per line. |
So the same command reads well for a person and parses cleanly for a script:
Tables fit the terminal width: on a narrow terminal the least useful columns drop first, and ids and addresses are never cut. Dates print as UTC. Nothing is redrawn after it prints, so the output stays in your scrollback.
Shaping JSON
--json forces JSON on a terminal too.
--fields a,b.c keeps only those fields, with dot paths for nested ones. On a list it applies to each item; on one object, to the object. In a table the fields become the columns.
--all on a list command follows next_page_token and returns every page merged into one document, so jq sees one array:
Without --all, list commands take --limit and --page-token as the API does.
Workflow commands print what a script needs. inbox new prints only the address when piped (--json for the whole inbox); wait --otp prints only the code. Both are made for $(...):
Streams: NDJSON
machinemail tail and machinemail events stream run until you stop them. Piped, they print one JSON object per line, {id, event, data}, flushed as each event arrives:
tail reconnects on its own and resumes from the last event it saw. To pick up where an earlier run stopped, pass the last id you processed as --last-event-id; --max n stops after n events (exit 0).
Raw and binary output
messages raw (the RFC 5322 source), messages download (an attachment), domains zone-file and inboxes export write the body as it comes. -o/--output <file> writes it to a file instead and prints {path, bytes, content_type}; --output <dir> keeps the server’s filename. The file is written through a temporary file with mode 0600 and renamed, so it is whole or absent. A binary body is refused on a terminal unless you pass --output or redirect stdout.
Input
Flags take typed values: integers and numbers are checked, booleans take --flag, --flag=false or --no-flag, arrays repeat the flag, and objects take JSON (--metadata '{"team":"ops"}'). --ttl-seconds null sends JSON null to clear a nullable field.
-d/--data gives the whole request body as JSON, @file, or - for stdin. Field flags override its keys:
The CLI reads stdin only when you ask for it with -: --data -, --text -, --api-key -, login --key -. An agent that runs it with an open, silent stdin never hangs.
Errors
Errors always go to stderr, so stdout carries only results. On a terminal they print as a short box with hints. When stdout is piped, with --json, or when stderr is not a terminal, an error is one line of JSON: the API’s error envelope with the HTTP status, or a CLI error (type usage or cli) with hints.
Exit codes
| Code | Meaning |
|---|---|
| 0 | Success. |
| 1 | API error not listed below (400, 409, 422, 5xx after retries), an unexpected failure, or a declined confirmation. |
| 2 | Usage: unknown command or flag, missing argument, bad value, or a delete without --yes and no terminal to ask on. Nothing was sent. |
| 3 | Auth: no API key, or the API answered 401 or 403. Also a sign-in denied in the browser. |
| 4 | Not found (404). |
| 5 | Rate limited (429), after retries. |
| 6 | Network: the API could not be reached (DNS, connection refused, TLS, timeout). |
| 7 | The config file is unreadable or invalid. |
| 8 | Timed out: wait reached --timeout, or a sign-in or claim code expired. |
| 130 | Interrupted (Ctrl-C). |
Branch on them in a script:
Retries
Rate-limited requests (429) are retried on every method, and server errors (500, 502, 503, 504) and network failures on GET, PUT and DELETE, up to 3 times. The CLI honors Retry-After, else waits 0.5 s, 1 s, 2 s and so on with jitter (at most 8 s). A request that sends mail is never retried after a server error, so a message is not sent twice. MACHINEMAIL_MAX_RETRIES changes the count; 0 turns retries off.
Deletes and confirmation
Every delete asks first on an interactive terminal: for inboxes, domains, pods, API keys and profiles you type the id, for smaller deletes (a message, a draft, a list entry) you answer y/N. Without a terminal to ask on (stdin or stderr not a terminal, or CI set) a delete exits 2 and sends nothing unless you pass -y/--yes:
Color, spinners and CI
- Color follows the terminal.
NO_COLOR(any value),--no-colororTERM=dumbturn it off;--colorforces it on. - Spinners appear only on an interactive stderr, and never with
CIset or--quiet. -q/--quietalso drops success lines; errors still print.-v/--verboselogs each request and response to stderr with keys, webhook secrets, claim tokens and agent tokens redacted, so verbose logs are safe to paste into an issue.CIset also means no prompts: deletes need--yes,loginprints the sign-in link instead of drawing the code box.
From an agent
An agent running shell commands gets the JSON behavior automatically, because its stdout is not a terminal. A few habits keep it reliable:
- Give it a key through
MACHINEMAIL_API_KEY(ideally scoped to its inbox) andMACHINEMAIL_INBOX, sosend,waitandreadneed no--fromor--inbox. - Use
readandwaitrather thanmessages get: their JSON is the agent view, with the agent-safe text inbodyanduntrusted: trueunless the sender authenticated. See Agent workflows. - Check the exit code rather than parsing error text, and read
hintsfrom the error JSON when it is not 0. - Pass
--yesexplicitly for deletes it is meant to do; without it a delete fails with exit 2 instead of hanging on a prompt. - Discover commands with
machinemail <resource> --helpandmachinemail <resource> <action> --help. Help is plain text without color when piped, and lists each command’s flags, examples and the API route it calls.