---
title: "Authentication"
description: "Sign the MachineMail CLI in through the browser or with an API key in CI, keep several keys in profiles, log out and revoke, and see where the key is stored."
canonical: "https://machinemail.dev/docs/cli/authentication"
---

# Authentication

> Sign the MachineMail CLI in through the browser or with an API key in CI, keep several keys in profiles, log out and revoke, and see where the key is stored.

Source: https://machinemail.dev/docs/cli/authentication

The CLI authenticates every request with an API key (`mm_live_...`), sent as `Authorization: Bearer`. There are three ways to get one into it:

| Situation                             | Use                                                                                                                                                                         |
| ------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| You, at a terminal, with a browser    | `machinemail login`: approve a code in the dashboard, get a new key.                                                                                                        |
| CI, scripts, containers               | An existing key in `MACHINEMAIL_API_KEY`, or saved with `login --key -`.                                                                                                    |
| An agent with no human and no account | `machinemail register`: a sandbox inbox and its own credentials. See [Agent workflows](https://machinemail.dev/docs/cli/agent-workflows#register-an-agent-without-a-human). |

## Sign in with the browser

`machinemail login` uses the OAuth 2.0 device authorization grant ([RFC 8628](https://www.rfc-editor.org/rfc/rfc8628)), the same flow as `gh auth login`: the CLI never sees your password or session, and you approve on a device where you are already signed in.

### Start the sign-in

```bash title="Terminal"
machinemail login
```

The CLI asks the API for a code and shows it:

```text
MACHINEMAIL Sign in
Confirm this code in the browser, signed in to MachineMail:

┌───────────┐
│ MLGH-JHRC │
└───────────┘

URL  https://machinemail.dev/cli?code=MLGH-JHRC

Press Enter to open the browser, or open the URL on any device. Ctrl-C cancels.
⠋ Waiting for approval
```

### Approve the code

Press Enter to open the link, or open it on any device: your laptop when the CLI runs over SSH, your phone, another browser. If you are not signed in to the dashboard, it asks you to sign in first (see the [Quickstart](https://machinemail.dev/docs/getting-started/quickstart) if you have no account yet).

The page shows the code, the access the key will get and when it was requested. Check that the code matches the one in your terminal, then choose **Approve**. **Deny** stops the CLI and creates nothing. Approve only a code you asked for yourself.

### The key is saved

The API creates a new organization key named `CLI on <hostname>` and hands it to the waiting CLI, once. The CLI saves it to the current profile:

```text
✓ Signed in as ada@acme.dev (Acme) — key 'CLI on ada-mbp' saved
PROFILE   default (current)
API       https://api.machinemail.dev
KEY       mm_live_…x7Qe (key_01m4g463jy0002nz2drf5r5bvx)
SAVED TO  /home/ada/.config/machinemail/config.json
```

The key has every permission except `inbound_raw`, and it is listed on the dashboard's [API keys](https://machinemail.dev/app/api-keys) page, where you can revoke it. Codes expire after 10 minutes and work once. [Connect an agent without code](https://machinemail.dev/docs/guides/connect-an-agent#review-and-revoke-access) lists everything you can review and revoke in the dashboard.

| Outcome                   | What the CLI does                                      | Exit code |
| ------------------------- | ------------------------------------------------------ | --------- |
| Approved                  | Saves the key and prints who you are signed in as.     | 0         |
| Denied in the browser     | `Sign-in was denied in the browser`. Nothing is saved. | 3         |
| Code expired (10 minutes) | `The sign-in code expired before it was approved`.     | 8         |
| Ctrl-C                    | `Interrupted; nothing was saved`.                      | 130       |

### Without a browser

When the CLI finds no browser to open (over SSH, in a container), it shows the code box without the Enter prompt: open the URL on any device. When stdin or stderr is not a terminal, or `CI` is set, it prints the link and the code as plain lines on stderr and waits:

```text
To sign in, open https://machinemail.dev/cli?code=HCQT-WVHJ
and confirm the code HCQT-WVHJ (expires in 10 minutes).
Waiting for approval...
```

`machinemail login --no-browser` never opens a browser, even when it could.

## Use an existing API key

Nobody is there to approve a code in CI. Create a key on the dashboard's API keys page (or with `machinemail api-keys create`), store it as a secret, and give it to the CLI in one of these ways. Each keeps the key out of your shell history and out of `ps`:

```bash title="Environment variable, read on every command"
export MACHINEMAIL_API_KEY=mm_live_...
machinemail inboxes list
```

```bash title="Saved to a profile, from stdin"
printf %s "$MACHINEMAIL_API_KEY" | machinemail login --key - --profile ci
```

```bash title="One command, from stdin"
printf %s "$MACHINEMAIL_API_KEY" | machinemail inboxes list --api-key -
```

```bash title="Pasted into a hidden prompt"
machinemail login --paste
```

`login --key`, `--with-key` (the same as `--key -`) and `--paste` check the key against `GET /v0/auth/me` before saving it, and print the same summary as `whoami`. A key the API rejects is not saved (exit 3). `--no-verify` saves without calling the API, for setting up a machine offline.

`--api-key <key>` also takes a literal key, but the CLI warns that it shows in `ps` and your shell history. Prefer the environment variable or stdin.

> **Note:** An organization key can do everything in your organization. For an agent or a CI job that works with one inbox, create a key scoped to that inbox, or to a pod. See [API
> keys](https://machinemail.dev/docs/concepts/api-keys).

### Which key is used

The first one found wins:

1. `--api-key`
2. `MACHINEMAIL_API_KEY`
3. the `api_key` of the profile in use

`MACHINEMAIL_API_KEY` therefore shadows a key saved by `login`. `machinemail doctor` warns when that happens, and `whoami` says where the key came from:

```bash title="Terminal"
machinemail whoami --json | jq '{key, key_source, organization_name}'
```

```json
{
  "key": "mm_live_…hdYv",
  "key_source": "env",
  "organization_name": "Acme"
}
```

## Profiles

A profile is a named API key. Use one per organization or agent, for example a second organization's key next to your own:

```bash title="Terminal"
machinemail login --profile work
printf %s "$CI_KEY" | machinemail login --key - --profile ci
machinemail inboxes list --profile work      # one command
export MACHINEMAIL_PROFILE=ci                # this shell
machinemail profiles use work                # from now on
machinemail profiles list
```

```text
PROFILE    API                          KEY
──────────────────────────────────────────────────────────
  ci       https://api.machinemail.dev  mm_live_…Rf2k
* default  https://api.machinemail.dev  mm_live_…hdYv
  work     https://api.machinemail.dev  mm_live_…a91Q
```

`*` marks the current profile. The profile in use is `--profile`, then `MACHINEMAIL_PROFILE`, then the config file's `current_profile`, then `default`. See [Configuration](https://machinemail.dev/docs/cli/configuration) for what else a profile holds.

`machinemail profiles delete <name>` removes a profile and its stored key (on a terminal you type the name to confirm; elsewhere pass `--yes`). It does not revoke the key.

## Log out

```bash title="Terminal"
machinemail logout
```

`logout` deletes the stored key from the profile; the profile itself stays. On a terminal it first asks whether to revoke the key on the server too (`DELETE /v0/api-keys/{id}`), so it stops working everywhere:

| Command                          | Removes the local copy | Revokes the key on the server    |
| -------------------------------- | ---------------------- | -------------------------------- |
| `machinemail logout`             | yes                    | asks on a terminal; no otherwise |
| `machinemail logout --yes`       | yes                    | yes                              |
| `machinemail logout --revoke`    | yes                    | yes                              |
| `machinemail logout --no-revoke` | yes                    | no                               |

```text
✓ Removed key mm_live_…hdYv from profile default and revoked it
```

If revoking fails, nothing is removed, so you can retry or revoke the key on the dashboard. `logout` only touches the config file: when `MACHINEMAIL_API_KEY` is set it says so, and you unset the variable yourself. For a profile created by `register`, `logout` also removes the agent credentials that renew its token.

Piped, `logout` prints what it did:

```json
{
  "profile": "default",
  "removed": true,
  "key": "mm_live_…hdYv",
  "revoked": true
}
```

## Where the key is stored

Keys saved by `login`, `register` and `config set api_key` live in the config file, by default `~/.config/machinemail/config.json` (`$XDG_CONFIG_HOME/machinemail/config.json` when that is set; `MACHINEMAIL_CLI_CONFIG` points at another file).

- The file is written with mode `0600` (read and write for you only) inside a `0700` directory, through a temporary file and a rename, so it is never half-written.
- `machinemail doctor` warns when the file is readable by other users.
- The key is stored in plain text, as with most CLIs: anyone who can read the file as you can use it. Do not commit it or copy it into images; in CI, use the environment variable.
- The CLI only ever prints keys masked (`mm_live_…hdYv`), including in `config list`, `whoami`, `--verbose` request logs and error messages. Agent tokens show as `(set)`.

See [Configuration](https://machinemail.dev/docs/cli/configuration#config-file) for the file's format.
