The CLI has two audiences: humans at a terminal and scripts parsing output. It serves both without compromise.
- stdout is data — the one pipeable artifact of a command: a table, a JSON document, a raw message body, a one-time token, a stream of event frames.
- stderr is everything else — success/
✓lines, warnings, prompts, progress, the "more results" hint, and the update notice.
They never intermix, so openemail … > out captures exactly the data and nothing
else, and a failed command still writes its data-or-nothing to stdout while the
error goes to stderr.
--json switches stdout to machine JSON. The shapes are a contract:
- Single resources are emitted as the object itself (
mailboxes get,messages get,domains get, …). - Lists are wrapped
{ "<plural>": [ … ], "nextCursor": "<cursor|"">" }.nextCursoris always present (empty string on the last page). Pass it back with--cursor, or use--allto drain every page client-side. - Delivery & append results (
send,deliver inbound,messages append,admin pickup ingest) are echoed as core's exact response bytes — so union discriminators (status),duplicate:false, and nulluid/uidValidity/threadIdare preserved verbatim rather than lost to Go'somitempty. - Timestamps are epoch seconds (as core emits them). Human output renders
them in local time;
--jsonkeeps the raw number. - Booleans are always real JSON
true/false(never0/1).
Pagination exceptions (mirroring core):
labels messagesis a UID window (--uid-min/--uid-max/--limit), not a cursor. Page forward with--uid-min <lastUid+1>.search --group-threadis single-page (a--cursorthere is rejected).labels list,sieve scripts list, andcredentials listare unpaginated.
| Code | Meaning |
|---|---|
0 |
success |
1 |
a runtime/API error (rendered on stderr) |
2 |
a usage error (bad flags/args; also non-TTY refusing to prompt) |
4 |
authentication required / failed (login needed, or a 401) |
A few commands use the exit code as a signal: sieve check exits 1 when a
script does not compile (the report is still emitted), deliver check exits 1
on a rejected recipient, and api exits 1 when the response status is ≥ 400.
Every error is core's { "error": "<snake_case_code>", … } envelope, surfaced as
error: <code> (HTTP <status>): <message> on stderr, plus any validation issues
or extra fields, plus a compile line:col when present. A 404 adds the honesty
note that the resource may simply not be accessible with the current key — core
never distinguishes "missing" from "not yours" (no existence leaks).