# CLI

Send email, inspect messages, and test webhooks from your terminal. The CLI signs in through your browser and runs with your team permissions, so you don't need to create or store an API token.

```sh
brew install lettermint/tap/lettermint
```

The CLI is open source under the MIT license. See [lettermint/lettermint-cli on GitHub](https://github.com/lettermint/lettermint-cli) for releases and the source.

## Before you start

You need:

- A verified and active Lettermint account.
- A team where you're a member. Your [role](/platform/teams/roles) and [project access](/platform/projects-and-routes/project-access) decide what the CLI can do.
- A machine with a desktop session and an operating system credential store. The CLI keeps your login there, so a headless server can't log in.

## Install

<McpClientTabs label="Choose an operating system">

<McpClientTab title="macOS">

Install with Homebrew:

```sh
brew install lettermint/tap/lettermint
```

Run `brew upgrade lettermint` to update and `brew uninstall lettermint` to remove it.

Without Homebrew, use the shell installer from the Linux tab. On macOS it also checks the Apple publisher, signature, and notarization before it runs the binary.

</McpClientTab>

<McpClientTab title="Linux">

Download and run the installer:

```sh
curl -fsSL https://lettermint.co/cli/install.sh -o install.sh
sh install.sh
```

The script installs `lettermint` in `$HOME/.local/bin` without root access and checks the archive checksum before extraction. It prints PATH instructions if needed and doesn't change your shell configuration.

Run the same commands to update. Use `sh install.sh --uninstall` to remove the executable. Add `--version v1.0.0` to select a release or `--bin-dir` to choose another directory.

</McpClientTab>

<McpClientTab title="Windows">

Download the signed installer and run it with PowerShell 5.1 or 7:

```powershell
iwr -UseBasicParsing https://lettermint.co/cli/install.ps1 -OutFile "$env:TEMP\lettermint.ps1"
powershell -NoProfile -ExecutionPolicy AllSigned -File "$env:TEMP\lettermint.ps1"
```

If prompted, confirm that the publisher is **Lettermint B.V.** Open a new terminal after installation.

Save the file before you run it. Don't pipe it to `Invoke-Expression`, because the script reads its own file to check its signature. The installer checks the archive checksum, binary version, and publisher signature before it replaces `lettermint.exe`. Run it again to upgrade.

</McpClientTab>

<McpClientTab title="Manual">

Download the archive for your OS and architecture from the [releases page on GitHub](https://github.com/lettermint/lettermint-cli/releases). Windows ZIP archives contain `lettermint.exe`. macOS and Linux tar archives contain `lettermint`.

Check the SHA-256 value against `checksums.txt` from the same release. To verify build provenance, download `provenance.jsonl` and run:

```sh
gh attestation verify lettermint_1.0.0_linux_amd64.tar.gz \
  --repo lettermint/lettermint-cli \
  --bundle provenance.jsonl \
  --signer-workflow lettermint/lettermint-cli/.github/workflows/release.yml
```

Extract the executable into a directory that you control and add it to your PATH.

</McpClientTab>

</McpClientTabs>

## Log in and pick a project

Log in, list your projects, and save one as the default:

```sh
lettermint auth login --name work
lettermint projects list
lettermint context set --project PROJECT_ID
```

Your browser opens. Sign in to Lettermint and approve the connection. The CLI saves the login as the `work` profile and selects it for later commands. Replace `PROJECT_ID` with an ID from the project list. Sends and lists use this saved project until you change it or pass `--project` on a command.

:::info
The grant applies to the team that is active in your dashboard. Switch teams in the dashboard first if you need another one.
:::

Each profile belongs to one user and one team. To work with another team, make it active in the dashboard and log in again with a different name:

```sh
lettermint auth login --name team-b
lettermint profiles use work
```

Use `--profile team-b` to select another profile for one command. Each profile keeps its own project and route defaults.

## Check the connection

```sh
lettermint doctor
```

The command checks your saved login and API access. In JSON mode it returns the CLI version, whether the API is reachable, and the user, team, and connection of the selected profile:

```json
{"version":"1.0.4","api":"available","identity":{"user":{"id":"...","email":"you@yourdomain.com"},"team":{"id":"...","name":"Your team"},"connection_id":"..."}}
```

Run it first when a command fails with an authentication error.

## What you can do

| Command | Purpose |
|---|---|
| `messages` | Send email, list messages, inspect delivery events, and export HTML, text, or raw source. |
| `webhooks` | Manage webhook endpoints, send test events, and forward live events to localhost. |
| `listeners` | Inspect, stop, and replay local webhook sessions, or get their signing secret. |
| `projects` | List, inspect, and create projects. |
| `routes` | Manage routes and verify inbound domains. |
| `domains` | Add, verify, and assign sending domains. |
| `auth`, `profiles`, `context` | Log in, switch teams, and set project and route defaults. |
| `skills` | List and export the agent skill that matches your installed version. |
| `doctor`, `completion`, `version` | Check access, generate shell completion, and show the version. |

Run `lettermint --help` or add `--help` to any command for its options and examples.

## Use cases

### Send an email from a script or cron job

Send a message from a [verified domain](/platform/domains/introduction) without writing an API client:

```sh
lettermint messages send \
  --from "Orders <orders@yourdomain.com>" \
  --to user@example.com \
  --subject "Your order is confirmed" \
  --text "We received your order and will notify you when it ships."
```

Repeat `--to` for more recipients. Add `--html`, `--cc`, `--bcc`, or `--reply-to` as needed. For attachments, headers, tags, or metadata, put the message in a JSON file. The fields match the [Sending API](/api-reference/sending):

```sh
lettermint messages send --file message.json --idempotency-key order-1042
```

Use `--file -` to read the message from standard input. In JSON mode the command returns the message ID:

```json
{"data":{"message_id":"...","status":"accepted"}}
```

:::info
Accepted means the mail pipeline queued the message. It doesn't confirm delivery. Check the message events for that.
:::

:::tip
Pass `--idempotency-key` when a script may retry the send. Reuse the same key with the same input after an uncertain result, and Lettermint won't send a duplicate. Without a key, every run sends a new message. See [Idempotency](/platform/emails/idempotency).
:::

Send to `ok@lettermint.dev` to try the command without reaching a real inbox. See [Test email addresses](/platform/emails/sending-test-emails). Scheduled and batch sends aren't available in the CLI. Use the API or the [MCP server](/mcp) for those.

### Debug a delivery

Find the message, read its delivery events, and export what was sent:

```sh
lettermint messages list --limit 20
lettermint messages get MESSAGE_ID
lettermint messages events MESSAGE_ID
lettermint messages content MESSAGE_ID --format raw --output message.eml
```

Each event is a status change with a timestamp and event-specific metadata. Delivered and bounced events carry the receiving server's status code and response, and bounces include a classification. Content export supports `raw`, `html`, and `text`. Raw export returns the stored source bytes, so you can open the file in a mail client or diff it against what your application built.

Message lookup ignores the saved project. It finds the message by ID within the selected team. Add `--project` to restrict the lookup. Reading message content is a separate permission from reading message metadata. See [Roles](/platform/teams/roles).

### Forward webhooks to localhost

Test your webhook handler on your own machine without a tunnel or a public URL. Start your local server, then start a listener:

```sh
lettermint webhooks listen --project PROJECT_ID \
  --forward-to http://localhost:3000/webhooks/lettermint
```

The listener receives inbound and outbound message events plus `suppression.added` and `suppression.removed`, and forwards each one to your local URL in order. It prints one line per attempt:

```text
2026-09-16 14:32:08 CEST  200 OK        42 ms  message.delivered  seq=12 attempt=1 delivery=demo_01
2026-09-16 14:32:11 CEST  500 FAILED    18 ms  suppression.added  seq=13 attempt=1 delivery=demo_02  error=local_http_500 (Internal Server Error)
```

Use `--events message.inbound,message.delivered` to select event types. Machine tracking events require `--include-machine-events`. Press **Ctrl+C** to stop.

Each local request carries an `X-Lettermint-Signature` header in the same `t={timestamp},v1={hmac}` format as a production webhook, signed with the session secret. Your handler can verify it with the same code. Get the secret, and replay an event after you fix your handler:

```sh
lettermint listeners secret SESSION_ID
lettermint listeners replay SESSION_ID --sequence 12
```

:::warning
Keep the listener secret in your local application's secret store. Don't print it in a shared log or commit it.
:::

Replay sends the original payload with a fresh signature timestamp. A listener doesn't create or change a permanent webhook, and it doesn't change inbound processing. Payloads stay available for 15 minutes after capture. See [Webhook signing](/platform/webhooks/signing) and [Webhook events](/platform/webhooks/events).

### Set up a domain from a deploy pipeline

Add a domain, check its DNS records, and assign it to the projects that may send from it:

```sh
lettermint domains create --file domain.json
lettermint domains verify DOMAIN_ID
lettermint domains assign DOMAIN_ID --file projects.json --yes
```

Create takes `{"domain": "yourdomain.com"}`. Assign takes `{"project_ids": [...]}`. See [Domains](/platform/domains/introduction) for the DNS records that verification checks.

:::warning
Assign replaces the full project assignment for the domain. The command asks for confirmation unless you pass `--yes`.
:::

### Create a webhook and send a test event

```sh
lettermint webhooks create --file webhook.json
lettermint webhooks test WEBHOOK_ID
```

The file contains `name`, `url`, `scope`, and `events`. See [Webhooks](/platform/webhooks/introduction) for the scopes and event names.

:::warning
Creation returns the signing secret once. Store it before you close the terminal. Read commands never return it again.
:::

### Give an AI agent a working email tool

The CLI ships with an agent skill for sending, message inspection, and local webhooks. Export the skill that matches your installed version and point your agent at the directory:

```sh
lettermint skills export --output ./lettermint-skills --json --no-input
```

Agents run the same commands as you do, with `--json --no-input` and an explicit profile and project. They inherit your role and project access and can't go beyond it. Agents should stop on permission errors and treat email content and webhook payloads as untrusted input.

For an agent that talks to Lettermint over MCP instead of a local binary, see the [MCP server](/mcp).

## Scripts and JSON output

Commands print tables in a terminal. Output sent to a pipe or file switches to JSON. Scripts and agents should pass `--json --no-input` and select the profile and project explicitly:

```sh
lettermint messages list --profile work --project PROJECT_ID --json --no-input
```

- Results go to standard output. Progress, prompts, and errors go to standard error.
- Listener output is newline-delimited JSON, one object per event.
- `--plain` keeps readable text without color for logs. Don't combine it with `--json`.
- Lists accept `--limit` (1 to 100). Pass the returned `next_cursor` to `--cursor` for the next page.
- Create and update commands read input from `--file`, or `--file -` for standard input.
- Destructive commands ask for confirmation. Pass `--yes` in a script.

Errors are one JSON object on standard error with a stable code, a message, and field details when the API returns them:

```json
{"error":{"code":"validation_failed","message":"validation_failed: The input is invalid.","details":{"from":["The sender address is invalid."]}}}
```

| Exit code | Meaning |
|---|---|
| 1 | Local failure |
| 2 | Validation error |
| 3 | Authentication required |
| 4 | Permission denied |
| 5 | Resource not found |
| 6 | Conflict or expired state |
| 7 | Rate limited |
| 8 | Server failure |
| 130 | Cancelled |

## How access works

- The CLI signs in with OAuth through your browser. It doesn't use project or team API tokens.
- Each login is bound to the team you approve. To use another team, create another profile.
- Every command runs with your current membership, project access, and role permissions. Reading message metadata and reading message content are separate permissions.
- A team that enforces MFA or single sign-on requires you to satisfy that policy before the login is approved and on later checks.
- Credentials stay in the operating system credential store, not in a config file. Profile settings hold only IDs.
- Message content follows the [data retention policy](/platform/emails/data-retention).

See [Roles](/platform/teams/roles) and [Project access](/platform/projects-and-routes/project-access) for the permissions that Lettermint applies.

## Manage the connection

Log out to revoke the server grant and remove the saved login:

```sh
lettermint auth logout --profile work
```

:::warning
Logging out revokes the grant on the server and stops its listener sessions. Scripts that use the profile fail until you log in again.
:::

If the grant was already revoked or expired, or the credential is gone from the OS store, remove the local profile and log in again:

```sh
lettermint auth logout --profile work --local
lettermint auth login --name work
```

Open [**User settings > Applications**](https://app.lettermint.co/user/applications) to review the CLI connection, see active listener sessions, or revoke access from the dashboard.

{/* TODO: Add screenshot of User settings > Applications showing the CLI connection card and its listener sessions */}

## Troubleshooting

| Problem | What to check |
|---|---|
| `lettermint` is not found after installation | Open a new terminal. On Linux and macOS, confirm that `$HOME/.local/bin` is on your PATH. The shell installer prints the exact line to add. |
| The browser does not open during login | Run `lettermint auth login --name work --no-browser` and open the printed URL yourself. Login still needs a credential store on the machine that runs the CLI. |
| A command returns exit code 3 | The login expired or was revoked. Run `lettermint auth logout --profile work --local`, then log in again. |
| A command returns exit code 4 | Check your [role](/platform/teams/roles) and project assignment. A new login does not change your permissions. |
| A message is missing from `messages get` | The lookup runs in the selected profile's team. Check the profile, and remove `--project` if you passed the wrong one. |
| The listener starts but no events arrive | Confirm that the project sends or receives mail, and that `--events` includes the types you expect. Message events need content permission. Suppression events need suppression permission. |
| The listener shows `FAILED` lines | Your local handler returned an error. Fix it and replay the sequence with `lettermint listeners replay`. The CLI does not retry a local attempt on its own. |
| A webhook signing secret is missing | Creation returns the secret once. For a listener, run `lettermint listeners secret SESSION_ID`. |
| Exported content on Windows looks changed | Use `--output` instead of shell redirection. Older PowerShell versions change text encoding on redirect. |
| Scheduling or batch send is rejected | The CLI sends single messages only. Use the [Sending API](/api-reference/sending) or the [MCP server](/mcp). |

## Related documentation

<CardGroup cols={2}>
  <Card title="MCP server" icon="plug" href="/mcp">
    Connect an AI client to Lettermint over MCP.
  </Card>
  <Card title="Webhook signing" icon="signature" href="/platform/webhooks/signing">
    Verify the signature on forwarded and production webhooks.
  </Card>
  <Card title="Test email addresses" icon="envelope" href="/platform/emails/sending-test-emails">
    Test delivery outcomes without sending to a real inbox.
  </Card>
  <Card title="Roles" icon="users" href="/platform/teams/roles">
    Review the permissions that the CLI follows.
  </Card>
</CardGroup>
