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
- Node.js 24 or newer
- An API key from the dashboard
- A verified sending domain for live mail — test mode needs no domain
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:
--api-key <key>on any command- the
MAILFULLY_API_KEYenvironment variable - 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
| Command | What it does |
|---|---|
emails batch <file> | Send up to 100 messages from a JSON array of API-shape objects |
emails list | List 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
| Command | What it does |
|---|---|
domains add <name> | Add a sending domain and print the DNS records to publish |
domains list | List 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|--disable | Turn 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
| Code | Meaning |
|---|---|
0 | Success |
1 | The API refused the request, or the network failed |
2 | Usage error — an unknown flag or a missing argument |
3 | Accepted, 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
- Quickstart — the API in five minutes
- Verify a domain — what
domains addandverifyare doing - Test mode —
mf_test_keys - Node.js SDK — the library the CLI is built on
