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.

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) to get the typed client rather than the command.

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)

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.

Sign in through your browser

mailfully login --web gets you a test key without copying anything. The CLI prints a link and a short code; open the link, sign in or sign up (Google or GitHub works), check that the code matches your terminal, and approve. The CLI then stores a mf_test_ key.

mailfully login --web                       # waits here until you approve
mailfully login --web --write-env .env      # also writes MAILFULLY_API_KEY into .env

The request is valid for 15 minutes. For scripts and agents, split it into two steps:

mailfully login --web --no-wait --write-env .env --json  # prints the link and code, exits at once
mailfully login --web --wait --timeout 90                # exits 0 once approved, 3 while still waiting

Other flags: --name <client> labels the request in the dashboard, and --force replaces a stored live key. A key made this way is a test key only; sending real mail still needs a verified domain and an approved account in the dashboard. It also cannot delete domains, change tracking, switch receiving or change templates, so do those in the dashboard.

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> --scheduled-at <iso>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> [--restart-if-failed]Poll status, or explicitly restart failed DKIM/MAIL FROM checks after correcting DNS
domains tracking <id> --enable|--disableTurn open/click tracking on or off

A pending check can be polled again without a flag. A failed check can also occur on a partially_verified domain; fix the records before using --restart-if-failed. A restart starts a new checking window and does not promise immediate verification. Domain dmarc_status and tracking_status are deprecated unmeasured values; not_started is not evidence of missing DNS or failed authentication. tracking_enabled is the open/click tracking setting.

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 and tags read the rollups and take --since and --until as ISO dates. analytics reputation shows the latest Gmail spam rate per domain.

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

Exit 3 means the request was accepted but every recipient was suppressed, so nothing sends (for emails batch, every message in the file). A partial batch exits 0. Without code 3, this would run the next step after a send that delivered nothing:

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

Treat any nonzero code as failure. More codes may be added in a minor release.

Error hints

When a send is refused with 429, the CLI prints a hint: for rate_limit_exceeded (and a 429 with no error type) it says to wait and retry, and for every other 429 it says retrying will not clear it. The API's behavior differs by type. rate_limit_exceeded clears in seconds. daily_quota_exceeded, volume_limit_reached and account_under_review carry Retry-After and clear as the rolling 24-hour window moves; account_under_review also ends when staff approve the account, and test keys keep working meanwhile. monthly_quota_exceeded, marketing_quota_exceeded and spend_cap_exceeded carry no Retry-After and clear only when the month rolls over, you upgrade the plan, or you raise the spend cap. A past-due account gets 402 payment_past_due until payment. See Rate limits and Errors.

What's next