Skip to the documentation
CLI

Scripting

JSON output, streams, exit codes, environment variables, and running unattended or in CI.

JSON output

With --json, stdout holds only JSON, indented by two spaces, while notes and progress stay on stderr, and nothing prompts. A list prints { items, hasMore, nextCursor }, an API object prints as the API returned it, and a hand-written command prints the object its help describes.

Terminal
openemail whoami --json | jq -r .workspaceIdopenemail emails list --status failed --json | jq -r ".items[].id"

An error goes to stderr as one line of JSON, and the exit code is the one a person would get:

stderr
{"error":{"type":"permission_error","code":"insufficient_scope","message":"This API key does not have the domains:write scope.","hint":"The credential is missing a scope this call needs. Use a key that has it, or sign in again with openemail login.","next":null,"status":403,"requestId":"req_7Hc2kQ","param":null,"docUrl":"https://openemail.uk/docs/api/errors#insufficient_scope","exitCode":4}}
FieldWhat it holds
typeThe API error type, or cli_error, network_error or internal_error for a failure inside the CLI
codeA stable code such as insufficient_scope, not_signed_in or unknown_flag
messageWhat went wrong, in one sentence
hint, nextWhat to try, and the command to run next, or null
status, requestId, param, docUrlFrom the API when the error came from it, otherwise null
exitCodeThe exit code the process ends with

Streams

Some output is a stream of JSON objects, one per line, so a pipeline can handle each item as it arrives:

  • A resource list with --all when stdout is not a terminal, or with --ndjson. --max <n> stops after that many items.
  • openemail temp watch --json, one line per new message.
  • openemail mcp serve, one JSON-RPC message per line in each direction.
Terminal
openemail contacts list --all > contacts.ndjsonopenemail emails list --status failed --all --max 500 | jq -r .id

Exit codes

CodeMeaning
0Done
1An unexpected failure, a server error, or a send that failed
2A usage error: a bad argument, an unknown command or flag, a value or confirmation that could not be asked for, or an origin or path the CLI will not send a credential to
3Not signed in, or the sign-in was refused, has expired or was signed out while the command ran
4Not allowed: a missing scope or permission, a verification code that could not be asked for or is paused, or an API key where a browser sign-in is needed
5Not found
6A conflict with the current state
7The input was invalid
8Rate limited, or the AI allowance is spent
9The network failed or timed out
10Cancelled: you declined a confirmation or a prompt
130, 143Stopped by Ctrl+C, or by SIGTERM

Environment variables

VariableWhat it does
OPENEMAIL_API_KEYAn API key to use instead of any saved profile
OPENEMAIL_PROFILEThe saved profile to use
OPENEMAIL_BASE_URLThe API origin for OPENEMAIL_API_KEY, --api-key and commands that send no credential. A saved sign-in only ever goes to the API it signed in to
OPENEMAIL_APP_URLThe web app origin, for sign-in, open and docs links
OPENEMAIL_CONFIG_DIRWhere profiles and inbox tokens are kept, ~/.openemail unless set
OPENEMAIL_NO_UPDATE_CHECKNever check npm for a newer release. OPENEMAIL_DISABLE_UPDATE_NOTICE does the same
NO_COLOR, FORCE_COLOR=0No colour
CINever prompt, never open a browser, never check for updates. Most CI services are recognised without it
VISUAL, EDITORThe editor send and reply open for a body

Unattended runs

The CLI prompts only when stdin and stdout are both terminals, and none of --json, --no-input or CI applies. Otherwise:

  • A missing required value stops with exit code 2 and names the flag to pass.
  • A destructive command stops with Refusing to run unattended. Pass --yes to confirm. and exit code 2, unless you pass --yes.
  • A change that needs a verification code stops with exit code 4, because nobody can type it. Use an API key, or run openemail verify first.

In CI

Give the job an API key with only the scopes it needs, keep it in a secret, and let OPENEMAIL_API_KEY carry it. Nothing is saved, nothing prompts, and no update check runs.

.github/workflows/deploy.yml
- name: Tell the team  env:    OPENEMAIL_API_KEY: ${{ secrets.OPENEMAIL_API_KEY }}  run: |    npx -y @openemail/[email protected] send \      --from [email protected] \      --to [email protected] \      --subject "Deployed ${{ github.sha }}" \      --text "Build ${{ github.run_number }} is live." \      --idempotency-key "deploy-${{ github.run_id }}"
Wait for a sign-up email
ADDRESS=$(npx -y @openemail/[email protected] temp new --ttl 15)./signup-test.sh "$ADDRESS"npx -y @openemail/[email protected] temp watch --first --json | jq -r .snippetnpx -y @openemail/[email protected] temp delete --yes
Fail on failed sends
failed=$(openemail emails list --status failed --json | jq ".items | length")test "$failed" -eq 0

Pass --idempotency-key on a send that a pipeline may retry, and derive it from what made the send necessary, such as a run id. Running the step again then returns the first send instead of mailing twice.

Your inbox,
on your own terms.

Email infrastructure for businesses, AI, agents and personal email. Built for scale, privacy and control. Everything email should have had from day one.

OpenEmail

Email infrastructure for businesses, AI, agents and personal email. Built for scale, privacy and control. Everything email should have had from day one.

© 2026 OpenEmail. All rights reserved.