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 one { items, hasMore, nextCursor } document, with --all too, on a terminal or in a pipe, and hasMore is true when --max stopped it. The few lists with a shape of their own, such as addresses list, say so in their help. An API object prints as the API returned it, and a hand-written command prints the object its help describes.
nextmail whoami --json | jq -r .workspaceIdnextmail 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:
{"error":{"type":"permission_error","code":"insufficient_scope","message":"This API key does not have the \"domains:write\" scope.","hint":null,"next":"Use an API key that has domains:write.","status":403,"requestId":"req_7Hc2kQ","param":null,"docUrl":"https://openemail.uk/docs/api/errors#insufficient_scope","exitCode":4}}| Field | What it holds |
|---|---|
| type | The API error type, or cli_error, network_error or internal_error for a failure inside the CLI |
| code | A stable code such as insufficient_scope, not_signed_in or unknown_flag |
| message | What went wrong, in one sentence |
| hint, next | What to try, and the command to run next, or null |
| status, requestId, param, docUrl | From the API when the error came from it, otherwise null |
| exitCode | The 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
--allpiped without--json, or with--ndjson.--max <n>stops after that many items. nextmail temp watch --json, one line per new message.nextmail mcp serve, one JSON-RPC message per line in each direction.
nextmail contacts list --all > contacts.ndjsonnextmail emails list --status failed --all --max 500 | jq -r .idExit codes
| Code | Meaning |
|---|---|
| 0 | Done, or a dry run that stopped before its first change |
| 1 | An unexpected failure, a server error, or a send that failed |
| 2 | A 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 |
| 3 | Not signed in, or the sign-in was refused, has expired or was signed out while the command ran |
| 4 | Not 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 |
| 5 | Not found |
| 6 | A conflict with the current state |
| 7 | The input was invalid |
| 8 | Rate limited, or the AI allowance is spent |
| 9 | The network failed or timed out |
| 10 | Cancelled: you declined a confirmation or a prompt |
| 130, 143 | Stopped by Ctrl+C, or by SIGTERM |
Environment variables
| Variable | What it does |
|---|---|
| NEXTMAIL_API_KEY | An API key to use instead of the active profile. --profile still picks a saved one |
| NEXTMAIL_PROFILE | The saved profile to use |
| NEXTMAIL_BASE_URL | The API origin for NEXTMAIL_API_KEY, --api-key and commands that send no credential. A saved sign-in only ever goes to the API it signed in to |
| NEXTMAIL_APP_URL | The web app origin a browser sign-in saves, and the one open and docs links use whatever profile is active, so a saved sign-in never refuses it. Unset, links use the web app the profile signed in through, a local web app when the API is local, or https://openemail.uk |
| NEXTMAIL_CONFIG_DIR | Where profiles and inbox tokens are kept, ~/.nextmail unless set |
| NEXTMAIL_NO_UPDATE_CHECK | Never check npm for a newer release. NEXTMAIL_DISABLE_UPDATE_NOTICE does the same |
| NO_COLOR, FORCE_COLOR=0 | No colour |
| CI | Never prompt, never open a browser, never check for updates. Most CI services are recognised without it |
| VISUAL, EDITOR | The 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
2and names the flag to pass. - A destructive command stops with
Refusing to run unattended. Pass --yes to confirm.and exit code2, 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 runnextmail verifyfirst. - A browser sign-in stops with exit code
2before anything is registered, so sign in withnextmail login --with-tokenorNEXTMAIL_API_KEYinstead.
An AI agent such as Claude Code or Codex can start from nextmail agents. The For AI agents page covers the same ground: help as data, dry runs, missing scopes and verification codes.
In CI
Give the job an API key with only the scopes it needs, keep it in a secret, and let NEXTMAIL_API_KEY carry it. Nothing is saved, nothing prompts, and no update check runs.
- name: Tell the team env: NEXTMAIL_API_KEY: ${{ secrets.NEXTMAIL_API_KEY }} run: | npx -y @nextmail/[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 }}"ADDRESS=$(npx -y @nextmail/[email protected] temp new --ttl 15)./signup-test.sh "$ADDRESS"npx -y @nextmail/[email protected] temp watch --first --json | jq -r .snippetnpx -y @nextmail/[email protected] temp delete --yesfailed=$(nextmail emails list --status failed --json | jq ".items | length")test "$failed" -eq 0Pass --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.