CLI
unitpost is the official command line: one command per API operation, generated from the same OpenAPI contract as the SDKs. Install it, sign in once, and send from your shell, your scripts, and your agents.
Install
One package, no runtime dependencies. On a machine you own, install it globally so unitpost is on your PATH. Anywhere else, run it without installing.
# Needs Node.js 20+. Zero runtime dependencies.
npm install -g unitpost-cli
# …or run it without installing
npx unitpost-cli@latest --helpSign in
On your own machine, unitpost login opens your browser and completes an OAuth 2.1 authorization-code flow with PKCE against the same authorization server the MCP server uses — one sign-in model covers both. The token lives in your OS keychain (a 0600 file where no keychain exists) and refreshes automatically. Narrow what you grant with repeated --scope flags; the default is the read-heavy surface plus emails:send.
unitpost login # browser sign-in (OAuth 2.1 + PKCE)
unitpost login --no-browser # prints the URL — for SSH / remote shells
unitpost login --key # paste a key instead (headless, CI-friendly)
unitpost whoami # which credential is in effect, and its scopesFirst commands
Commands read as unitpost <resource> <action>. Required fields are flags; ids are positional. Append --help to any command to see its flags — required-ness, enum values, and the spec’s own descriptions included.
unitpost whoami # credential check that actually hits the API
unitpost --help # all 82 commands
unitpost email send --help # flags for one command
unitpost doctor # diagnose the whole setup in one commandCredentials
Every run resolves one credential, in this order: --api-key, then UNITPOST_API_KEY, then your saved login. An environment variable always wins over a saved login — which is what you want on a shared machine or in CI. unitpost logout removes the local credential and revokes the server grant (best-effort offline).
Profiles
Each sign-in is a named profile. Keep one per workspace or environment and switch between them without re-authenticating. The CLI never prints a secret — listings show a masked form.
unitpost login --profile staging # a second credential
unitpost auth list # all profiles, masked
unitpost auth switch staging # change the default
unitpost email list --profile staging # one command, another profileCommands
The tree is generated from the OpenAPI snapshot — 82 commands, one per operation — so a new endpoint becomes a documented, shell-completable command with no CLI code change, and drift fails CI. A few convenience flags (--html-file, --attach, --var, --file) rewrite into real flags for things shells express badly. One deliberate absence: unitpost api-keys … explains that API keys can only be managed in the dashboard, because those endpoints take a session, not a Bearer token.
Global flags
Every command accepts these. Environment fallbacks where they exist.
| --api-key | One-off credential. Prefer the env var or a profile — flags leak into shell history and ps. |
|---|---|
| -p, --profile | Use a named saved credential for one command. |
| --base-url | Override the API origin (or UNITPOST_BASE_URL). |
| --json | Machine output: exactly one JSON document on stdout, notes on stderr. |
| -q, --quiet | Implies --json and prints only the result document. |
| -y, --yes | Answer yes to confirmations. Required in non-interactive shells. |
| --debug | Log method, path, status, and request id to stderr (or UNITPOST_DEBUG). |
| --dry-run | Validate and print the resolved request. Exit 0, no network. |
| --idempotency-key | Override the auto-generated key on writes. |
| --body | Raw JSON document, inline, from @file, or from stdin with -. |
| --no-color | Plain output (also honors NO_COLOR and dumb terminals). |
Output modes
Two modes, picked for you. On a terminal you get formatted, colored output and tables. When stdout is piped, CI is set, or you pass --json, you get exactly one JSON document on stdout and nothing else — progress always goes to stderr, so a redirect is always valid JSON. Exit codes: 0 success, 1 the request failed, 2 the command itself was wrong (with a closest-match suggestion).
# Pipe straight into jq — stdout is only ever the result document
unitpost email list --limit 100 | jq -r '.data[] | select(.status=="bounced") | .to'
# Capture an id for the next step
id=$(unitpost email send --from you@yourdomain.com --to a@acme.com \
--subject Hi --text Hi --json | jq -r .id)
# Preview a send without touching the network
unitpost email send --from you@yourdomain.com --to a@acme.com \
--subject Hi --text Hi --dry-runSafety
Destructive commands ask first and refuse to guess in a non-interactive shell without --yes. Every write carries an idempotency key — generated for you, overridable with --idempotency-key — so a retried request replays the original result instead of sending twice. Preview anything with --dry-run: it validates and prints the resolved request, then exits 0 without touching the network. Shell completion covers commands and flags:
# Tab completion for every command and flag, generated from the API catalog
eval "$(unitpost completion bash)" # or: zsh
unitpost completion fish > ~/.config/fish/completions/unitpost.fishAgents
The CLI is built to be driven. The agent skill teaches the mapping (email_send ⇒ unitpost email send), JSON-on-stdout and stable exit codes make output parseable, and --dry-run lets an agent show its work before anything sends. Prefer MCP for tool calls inside a chat session and the CLI for shells, scripts, and CI — same operations, same scopes, same gates either way.
# CI: no login step, just the key
export UNITPOST_API_KEY="pk_live_..."
unitpost email send --from you@yourdomain.com --to ops@acme.com \
--subject "Deploy finished" --text "All green."CI
No login step: put the key in the environment, pass --yes where a command would confirm, and gate the job on unitpost doctor, which exits non-zero when anything is off. The CLI checks for updates about once a day on stderr — silence it with NO_UPDATE_CHECK=1.
Troubleshooting
Start with unitpost doctor: version, credential source, storage, base URL, API acceptance, and sending domain in one pass. Credential rejections name the fix (unitpost whoami, unitpost login). Every failure carries the API request id — rerun with --debug to see method, path, status, and id on stderr.