Official agent-first command-line interface for the Brew public API — manage contacts, email designs, campaigns, automations, audiences, domains, and analytics from the terminal, a script, or an AI agent.
Built as a thin shell over @brew.new/sdk:
the SDK carries the typed transport (retries, idempotency, error
envelopes, types generated from the canonical OpenAPI spec), the CLI adds
the command surface and the agent contract. One source of truth end to
end: Zod contracts → OpenAPI → SDK → CLI, drift-gated at every hop.
npm install -g @brew.new/cli # or run without installing:
npx @brew.new/cli --helpbrew install getbrew/tap/brew-cli # standalone binary (macOS/Linux)Prebuilt binaries for macOS (arm64/x64), Linux (x64/arm64), and Windows are attached to every GitHub Release.
brew-cli login # paste an API key from https://brew.new/settings/api
brew-cli doctor # trust check: auth, reachability, CLI-vs-API drift (exit-code gated)
brew-cli whoami
brew-cli contacts search --filter email:equals:jane@example.com
brew-cli emails list --limit 10
brew-cli emails send em_123 --test --to you@company.com
brew-cli content upload-image ./logo.png # a local image into the brand libraryEvery command supports --json (automatic when stdout is piped), prints
data on stdout and progress on stderr, and documents itself:
brew-cli emails send --help| Precedence | Source |
|---|---|
| 1 | --api-key <key> flag |
| 2 | BREW_API_KEY environment variable |
| 3 | ~/.config/brew-cli/config.json (written by brew-cli login, mode 0600) |
Keys are created at brew.new/settings/api.
A key is scoped either to ONE brand or to the whole organization.
Organization-scoped keys must name a brand on brand-scoped commands:
--brand <brandId>, BREW_BRAND_ID, or brew-cli config set brandId ….
Other environments: --api-url http://localhost:3000/api or
BREW_API_URL.
Designed for AI agents as first-class users:
- Structured everything —
--jsonprints API envelopes verbatim; auto-enabled when stdout is not a TTY. Errors are JSON envelopes on stderr with stablecodes, thex-request-id, anddetailswhen the API names the offending fields (a trigger fire's400 INVALID_PAYLOAD). - Semantic exit codes —
0ok ·1API/runtime ·2usage ·3auth ·4confirmation required ·130interrupted (SIGINT) ·143terminated (SIGTERM). After printing its envelope an interrupted CLI re-raises the signal, so a shell loop around it stops too. - Every request is bounded — the deadline covers the response body,
not only the headers.
--timeout <duration>(90s,1500ms,5m) caps the whole command, retries and--allpages included; long-running commands (emails generate,content gif, …) default to the SDK's own per-call budget.--max-retries <n>tunes the retry loop. Ctrl-C stops a request wherever it is. - Replay, don't repeat — the API keeps working after the CLI
disconnects, so a write that timed out, lost its connection or was
interrupted may still complete. Its envelope carries the
idempotencyKeyit was sent with and aretryCommandthat replays it (the first attempt's result, not a second write) instead of running it twice. The replay needs the API's idempotency store: while it is degraded, writes other than sends run without it, so for a write that must not happen twice (creating a brand, say) pass--max-retries 0and check whether it landed before you re-run it. - Confirmation protocol — irreversible commands (sends, deletes,
trigger fires) never hang waiting for input: non-interactive callers
get exit
4plus a JSON envelope containing a ready-to-runconfirmCommand; pass--yesto proceed. Humans on a TTY get a y/N prompt. Test sends (emails send --test) skip the gate. - Saved email recovery —
409 EMAIL_RESULT_UNAVAILABLEmeans the generation or edit committed. Do not repeat the write. Read the exact saved result usingerror.details.emailIdandemailVersionId:brew-cli emails get <emailId> --email-version-id <emailVersionId> --include html. The error preserves these identifiers and the API's recovery guidance. - No fuzzy matching — unknown commands fail hard with exit 2.
- Machine-readable discovery —
brew-cli docs --agentprints the full manifest (commands, flags, classes, routes, env, exit codes);brew-cli docs apifetches the liveGET /v1/helpcatalog;brew-cli api <method> <path>is the raw escape hatch for anything the CLI has no dedicated command for yet. - Pipes and stdin —
--input <json|->and--file <path|->everywhere bodies or files are needed;--alldrains cursor pagination;--idempotency-keymakes retries safe across process restarts.
Filtered list pages can be empty while more pages remain. Follow their cursor
or use --all. When --all receives a repeated or cyclic cursor, it exits with code 1 and
CLI_PAGINATION on stderr. It emits no truncated success payload. Error details
include the fetched page and item counts; a cyclic cursor is not offered as a
resume point. This also covers resumed lists and a comment thread's older
message slices. Empty pages with an advancing cursor continue normally.
The full generated reference lives in
docs/commands/README.md. Resource groups:
contacts, fields, emails (+ groups, comments), sends,
audiences, automations (+ triggers, runs, audience-runs,
trigger-instances), analytics, insights, brand, brands, content,
templates, flows, chats, notifications, plus
canvas sessions and login/logout/whoami/config/usage/health/docs/api.
For an embedded editor, create brands with an organization-scoped key,
poll brands get <brandId> until ready: true, then use --brand <brandId>
to generate or import emails and create scoped canvas sessions grants.
Keep API keys on the host's backend; pass only the scoped session credentials
to the browser. See the white-label integration guide
for the complete SDK and host authorization flow. Imports accept HTML, MJML,
JSX, and saved EML messages; JSON output includes the import's assetReport
with asset counts and warnings.
Every collection with ids to look up has a real detail read — <group> get <id> returns the bare row, and an unknown id surfaces the API's own typed
404. notifications list and emails comments list are list-only (one
comment thread is emails comments list <emailId> --comment-id <id>).
The app repo's Zod contracts generate the OpenAPI spec, which is mirrored
into this repo (openapi/public-api-v1.yaml) and the SDK byte for byte.
Two parity tests gate CI here:
- SDK parity walks the installed
@brew.new/sdkclient and fails if any method lacks a command or a reviewed skip entry — so an SDK upgrade that ships new surface fails the build until the CLI ships it too. - Spec parity walks the vendored spec and fails on any operation without a command route or a reviewed skip — including operations the SDK itself has not wrapped yet, making this repo the outermost completeness sentinel of the whole chain.
- The nightly spec-drift sentinel downloads the live published spec and diffs it against the vendored copy, opening a labeled issue the day the real API grows past this CLI.
brew-cli doctorcloses the loop at runtime: it diffs the installed command surface against the server's liveGET /v1/helpcatalog and exit-codes the verdict, so agents can gate work on a current, authed, in-sync CLI. Agents: seeskills/brew-cli/SKILL.md.
Contributor guide: AGENTS.md · Release process:
RELEASING.md.
Development and release checks use Bun 1.3.10, pinned in every workflow.
Command metadata is supplied by CLI composition to docs and doctor; these
handlers do not import the live registry.
MIT