Skip to content
machinemail

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:

stdoutOutput
A terminalTables 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 --jsonOne 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:

Terminal
machinemail messages list support-agent@machinemail.dev
text
ID                              FROM               SUBJECT             TRUST    RECEIVED
────────────────────────────────────────────────────────────────────────────────────────────────────
msg_01m4gnfsw200001e83690x4dm8  noreply@acme.dev   Your Acme verific…  UNKNOWN  2026-10-09 15:46 UTC
msg_01m4gnf4m40000ar9pc867d5at  → ada@example.com  Hello               -        2026-10-09 15:45 UTC
2 messages
Terminal
machinemail messages list support-agent@machinemail.dev | jq -r '.messages[].subject'
text
Your Acme verification code
Hello

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.

Terminal
machinemail messages list "$INBOX" --fields message_id,from,subject
json
{
  "count": 2,
  "limit": 50,
  "messages": [
    {
      "message_id": "msg_01m4gnfsw200001e83690x4dm8",
      "from": "noreply@acme.dev",
      "subject": "Your Acme verification code"
    },
    {
      "message_id": "msg_01m4gnf4m40000ar9pc867d5at",
      "from": "support-agent@machinemail.dev",
      "subject": "Hello"
    }
  ]
}

--all on a list command follows next_page_token and returns every page merged into one document, so jq sees one array:

Terminal
machinemail inboxes list --all | jq -r '.inboxes[].inbox_id'

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 $(...):

Terminal
INBOX=$(machinemail inbox new --ephemeral --ttl 30m)
CODE=$(machinemail wait --inbox "$INBOX" --otp)

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:

Terminal
machinemail tail --inbox "$INBOX" | jq -c '{event, from: .data.message.from, subject: .data.message.subject}'
json
{"event":"message.received","from":"noreply@acme.dev","subject":"Your Acme verification code"}
{"event":"message.sent","from":"support-agent@machinemail.dev","subject":"Ping"}

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:

Terminal
jq -n --arg to ada@example.com '{to: [$to], subject: "Report", text: "Attached."}' \
  | machinemail messages send "$INBOX" --data -

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.

Terminal
machinemail messages get "$INBOX" msg_nope
text
{"error":{"type":"not_found","message":"Message not found: msg_nope"},"status":404}
text
{"error":{"type":"usage","message":"Unknown action \"lst\" for messages"},"hints":["Did you mean list?"]}

Exit codes

CodeMeaning
0Success.
1API error not listed below (400, 409, 422, 5xx after retries), an unexpected failure, or a declined confirmation.
2Usage: unknown command or flag, missing argument, bad value, or a delete without --yes and no terminal to ask on. Nothing was sent.
3Auth: no API key, or the API answered 401 or 403. Also a sign-in denied in the browser.
4Not found (404).
5Rate limited (429), after retries.
6Network: the API could not be reached (DNS, connection refused, TLS, timeout).
7The config file is unreadable or invalid.
8Timed out: wait reached --timeout, or a sign-in or claim code expired.
130Interrupted (Ctrl-C).

Branch on them in a script:

Terminal
if CODE=$(machinemail wait --inbox "$INBOX" --otp --timeout 2m); then
  echo "code: $CODE"
else
  case $? in
    8) echo "no code within 2 minutes" >&2 ;;
    3) echo "check MACHINEMAIL_API_KEY" >&2 ;;
    *) echo "wait failed" >&2 ;;
  esac
  exit 1
fi

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:

Terminal
machinemail inboxes delete "$INBOX" --yes

Color, spinners and CI

  • Color follows the terminal. NO_COLOR (any value), --no-color or TERM=dumb turn it off; --color forces it on.
  • Spinners appear only on an interactive stderr, and never with CI set or --quiet.
  • -q/--quiet also drops success lines; errors still print.
  • -v/--verbose logs 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.
  • CI set also means no prompts: deletes need --yes, login prints 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) and MACHINEMAIL_INBOX, so send, wait and read need no --from or --inbox.
  • Use read and wait rather than messages get: their JSON is the agent view, with the agent-safe text in body and untrusted: true unless the sender authenticated. See Agent workflows.
  • Check the exit code rather than parsing error text, and read hints from the error JSON when it is not 0.
  • Pass --yes explicitly 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> --help and machinemail <resource> <action> --help. Help is plain text without color when piped, and lists each command’s flags, examples and the API route it calls.