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

> **For AI agents:** the complete documentation index is at [llms.txt](/docs/llms.txt). Append `.md` to any page URL for its markdown version.

The [`mailfully`](https://www.npmjs.com/package/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

- Node.js 24 or newer
- An API key from the [dashboard](https://dashboard.mailfully.com)
- A [verified sending domain](/guides/verify-a-domain) for live mail — test mode needs no domain

## Install

```bash
npm install -g mailfully
```

The same package is also the [Node.js SDK](/sdks/node) — 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:

```bash
mailfully --version
```

## Quick start

```bash
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`.

<Warning>
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](/guides/test-mode).
</Warning>

## 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.

```bash
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>` | 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>` | 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](/guides/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:

```bash
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 |

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:

```bash
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](/concepts/rate-limits) and [Errors](/concepts/errors).

## Examples

```bash
# 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

- [Quickstart](/quickstart) — the API in five minutes
- [Verify a domain](/guides/verify-a-domain) — what `domains add` and `verify` are doing
- [Test mode](/guides/test-mode) — `mf_test_` keys
- [Node.js SDK](/sdks/node) — the library the CLI is built on
