Mailfully logo

CLI

Install the mailfully CLI, authenticate once, and send mail, verify domains, inspect the message log, and read analytics without leaving the terminal.

The mailfully CLI wraps the same API the SDKs call, so anything you can do with an API key you can do from a shell script or a terminal session. It is built for two audiences: people setting an account up by hand, and scripts that need machine-readable output and dependable exit codes.

Prerequisites

Install

npm install -g mailfully

The same package is also the Node.js SDK — install it as a project dependency instead (npm install mailfully) and you get the typed client rather than the command. The CLI is a terminal front-end over exactly that client, so the two never disagree about what the API accepts.

Verify it resolved:

mailfully --version

Quick start

mailfully login
mailfully send \
  --from orders@mail.acme.com \
  --to you@yourcompany.com \
  --subject "Your order shipped" \
  --text "Your order is on its way."

A successful send prints the message id and the environment the key belongs to:

Accepted: 01J9ZC3AB8XQ4RW2N7VKT5EMHD (live)

The environment is on every accept line on purpose. A test key and a live key look alike, and the moment you most need to know which one you used is the moment mail either does or does not reach a real person.

Authenticate

mailfully login verifies the key before storing it, so a typo fails immediately rather than on your next send. It writes to ~/.config/mailfully/config.json.

The CLI resolves a key in this order, first match wins:

  1. --api-key <key> on any command
  2. the MAILFULLY_API_KEY environment variable
  3. the key stored by mailfully login

Use MAILFULLY_API_KEY in CI rather than committing a stored config. Check what you are pointed at with mailfully whoami, and clear a stored key with mailfully logout.

A mf_live_ key sends real mail to real people and bills for it. When you are experimenting, use a mf_test_ key — it routes to the mailbox simulator, never affects your reputation, and is never metered. See Test mode.

Commands

Send

mailfully send composes one message from flags. Bodies can be inline (--html, --text) or read from disk (--html-file, --text-file). --to, --cc, --bcc, --reply-to, --var, --tag, and --header are repeatable.

mailfully send \
  --from orders@mail.acme.com \
  --to you@yourcompany.com \
  --template tmpl_01J9Z8QK2M \
  --var order_id=A-4471 \
  --tag category=order-shipped \
  --idempotency-key "order-A-4471-shipped"

Pass --scheduled-at (ISO 8601, up to 90 days out) to schedule, and --type marketing for campaign mail — which accepts exactly one recipient across to/cc/bcc.

emails

CommandWhat it does
emails batch <file>Send up to 100 messages from a JSON array of API-shape objects
emails listList sent mail, newest first
emails get <id>One message's detail
emails events <id>One message's event timeline
emails cancel <id>Cancel a scheduled message that has not been sent
emails reschedule <id>Move a scheduled message to a new time

emails list filters on --status, --tag, --domain, --recipient, --search, --since, and --until. Paginate with --limit and --cursor, or pass --all to walk every page.

domains

CommandWhat it does
domains add <name>Add a sending domain and print the DNS records to publish
domains listList this org's domains
domains get <id>One domain's detail and DNS records
domains verify <id>Re-check the domain's DNS now
domains tracking <id>Turn open/click tracking on or off

templates

templates create, list, get, update, and delete. Send with a stored template using send --template <id> --var name=value. See Templates.

suppressions and analytics

suppressions list shows suppressed recipients — read-only, because adding and removing entries lives in the dashboard.

analytics daily, domains, tags, and reputation read the rollups. All four take --since and --until as ISO dates.

Script against it

Every command takes --json, which prints the API's raw response on stdout and nothing else. Human-readable hints and every error go to stderr, so a --json invocation is always safe to pipe:

mailfully emails list --status bounced --since 2026-09-01 --all --json \
  | jq -r '.data[].to[]' | sort -u

Exit codes

CodeMeaning
0Success
1The API refused the request, or the network failed
2Usage error — an unknown flag or a missing argument
3Accepted, but nothing will be sent

Code 3 is the one worth handling deliberately. It means the API accepted the request and will dispatch no mail, because the recipients were suppressed — either a send whose recipients were all suppressed, or an emails batch where every row was canceled. A partial batch exits 0, because some mail did go out.

It is distinct from 1 on purpose: 1 is the API refusing you, 3 is the API accepting you and sending nothing. Without it, this would silently do the wrong thing:

mailfully send --from --to --subject --text && ./mark-as-notified.sh

Treat any nonzero code as failure rather than testing for 1 — more codes may be added in a minor release.

Error hints

When a send is refused, the CLI explains what will actually clear the refusal. This matters most for 429, which the API returns for six different reasons: only the per-second rate limiter clears by waiting. A daily quota, a send cap, or an account under human review will not, and the CLI says so rather than suggesting a retry that cannot work. See Rate limits and Errors.

Examples

# Verify a new sending domain
mailfully domains add mail.acme.com
mailfully domains verify dom_01J9Z8QK2M

# Ship a batch from a file
mailfully emails batch ./receipts.json

# Investigate a delivery
mailfully emails list --search you@yourcompany.com
mailfully emails events 01J9ZC3AB8XQ4RW2N7VKT5EMHD

# Watch deliverability
mailfully analytics daily --since 2026-09-01
mailfully analytics reputation

What's next