Skip to content
machinemail

Authentication

Sign the CLI in through the browser with the device flow, give it an API key in CI, keep several keys in profiles, log out and revoke, and see where the key is stored and who can read it.

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

SituationUse
You, at a terminal, with a browsermachinemail login: approve a code in the dashboard, get a new key.
CI, scripts, containersAn existing key in MACHINEMAIL_API_KEY, or saved with login --key -.
An agent with no human and no accountmachinemail register: a sandbox inbox and its own credentials. See Agent workflows.

Sign in with the browser

machinemail login uses the OAuth 2.0 device authorization grant (RFC 8628), 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.

  1. Start the sign-in

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

  3. 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 page, where you can revoke it. Codes expire after 10 minutes and work once.

OutcomeWhat the CLI doesExit code
ApprovedSaves the key and prints who you are signed in as.0
Denied in the browserSign-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-CInterrupted; 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:

Environment variable, read on every command
export MACHINEMAIL_API_KEY=mm_live_...
machinemail inboxes list
Saved to a profile, from stdin
printf %s "$MACHINEMAIL_API_KEY" | machinemail login --key - --profile ci
One command, from stdin
printf %s "$MACHINEMAIL_API_KEY" | machinemail inboxes list --api-key -
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.

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:

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 and base URL. Use one per organization, environment or agent:

Terminal
machinemail login --profile staging --base-url https://staging.example.dev
printf %s "$CI_KEY" | machinemail login --key - --profile ci
machinemail inboxes list --profile staging      # one command
export MACHINEMAIL_PROFILE=ci                   # this shell
machinemail profiles use staging                # 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
  staging  https://staging.example.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 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

Terminal
machinemail logout

logout deletes the stored key from the profile; the profile and its base URL stay. 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:

CommandRemoves the local copyRevokes the key on the server
machinemail logoutyesasks on a terminal; no otherwise
machinemail logout --yesyesyes
machinemail logout --revokeyesyes
machinemail logout --no-revokeyesno
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 for the file’s format.