Skip to the documentation
CLI

Authentication

Sign in with your browser or an API key, keep several profiles, and verify a code before a sensitive change.

Two ways to sign in

Run openemail login in a terminal and it asks which you want. Either way the sign-in is saved as a profile, and every later command uses the active one.

CommandActs asVerification codes
openemail loginYou, in the workspace and with the access you approveAsked before a few sensitive changes
openemail login --with-tokenThe workspace, with the scopes the key holdsNever asked
  • Only a browser sign-in can use ai compose, ai summarize and the MCP commands.
  • A browser sign-in lasts until the approval you chose expires, or until you sign out. A key works until it is revoked.

Browser sign-in

  1. openemail login registers a new app for this sign-in, named OpenEmail CLI on <your computer>, and opens the OpenEmail approval page in your browser. If the browser does not open, use the link it prints.
  2. Sign in if you need to, then choose the workspace, the access the CLI gets (read, read and send, full, or your own set of permissions), the domains or addresses it reaches, and how long the approval lasts.
  3. Approve. The browser hands the approval back to the terminal on its own, and you can close the tab. The CLI prints who you are signed in as, the workspace, and when the approval expires.
Terminal
openemail loginopenemail login --scopes emails:send,threads:readopenemail login --profile work
  • The CLI waits 10 minutes for you to approve. Choosing Not now on the approval page cancels the sign-in, with exit code 10.
  • --scopes preselects permissions on the approval page, and you can still change them there.
  • When the profile already holds a sign-in, a terminal asks before replacing it. Unattended it refuses, unless you pass --force or --yes. Replacing a browser sign-in revokes the old one.

Each browser sign-in is its own connected app, listed in Account → Connected apps with the access you approved, where you can change or remove it. openemail open apps opens that page.

Underneath is the OAuth flow the MCP server uses: a public client with PKCE, a one-time code, and an access token that lasts an hour and is refreshed for you. The browser returns to 127.0.0.1 on a random port, and only the code of this sign-in is accepted there.

Over SSH, or without a browser

When the CLI cannot open a browser on this machine, it prints the link instead: over SSH, in CI, on Linux without a display, or when you pass --no-browser. Open the link in a browser on any device and approve. The page then shows a sign-in code, which you paste into the terminal.

Terminal
$ openemail login --no-browserOpen this link in a browser on any device to sign in:  https://api.openemail.uk/auth/mcp/authorize?response_type=code&client_id=…Paste the code from your browser
  • A code works only for the sign-in that printed the link, so a code from another tab is refused.
  • Pasting the whole address the browser ended on works too.
  • Without a terminal, pipe the code in on stdin.

API keys

An API key signs a script in with no browser, and is never asked for a code. Create one in Settings → API keys (openemail open api-keys) with only the scopes the script needs. The CLI checks the key with GET /keys/self before saving it, and takes oe_live_ and oe_test_ keys. Mail sent with a test key is never delivered.

Terminal
openemail login --with-token < ~/.config/openemail/keyecho "$OPENEMAIL_KEY" | openemail login --with-token --profile ciopenemail login --token oe_live_…

--token works too, but the key lands in your shell history, so the CLI warns you and suggests --with-token. Two ways use a key without saving it:

  • OPENEMAIL_API_KEY in the environment is used by every command that sees it, ahead of any saved profile.
  • --api-key <key> is used for that one command.

When there is more than one credential, the first of these wins: --api-key, OPENEMAIL_API_KEY, the profile named by --profile, the profile named by OPENEMAIL_PROFILE, then the active profile.

Profiles

A profile is one saved sign-in, of either kind. The first is called default. Sign more in with --profile, and switch between them:

Terminal
openemail login --profile workopenemail profile listopenemail profile use workopenemail inbox --profile defaultOPENEMAIL_PROFILE=work openemail statusopenemail profile currentopenemail profile remove work
  • profile list shows each profile with its kind, workspace and user or key, and marks the active one. Its JSON never includes a token or a key.
  • profile current prints only the name on stdout, so $(openemail profile current) works in a script.
  • profile remove <name> is the same as openemail logout --profile <name>.
  • A profile name is up to 64 letters, digits, dots, dashes and underscores.
  • profile use is also profile switch. Removing or signing out of the active profile leaves no profile active, and the next command that needs a sign-in points at openemail profile use <name>.

Which API a sign-in talks to

A saved profile remembers the API it signed in to, and its credential is only ever sent there. A --base-url or OPENEMAIL_BASE_URL that names another origin stops the command with exit code 2 before anything is sent, and says how to sign in to that origin as a profile of its own.

Terminal
openemail login --profile other --base-url https://api.example.comopenemail inbox --profile other
  • A key from OPENEMAIL_API_KEY or --api-key is not a saved profile, so it goes to the origin in --base-url or OPENEMAIL_BASE_URL, or to https://api.openemail.uk when neither is set.
  • Commands that send no credential follow --base-url and OPENEMAIL_BASE_URL whatever profile is active: disposable inboxes, methods that need no key, docs and open.
  • Plain http is refused for every origin except localhost, 127.0.0.1 and ::1, with exit code 2: the API, the web app, the sign-in, token and revoke requests, and the MCP server. Use https for anything else.
  • A request path that would leave the API origin, such as openemail api //example.com/x, stops with exit code 2 and invalid_path before anything is sent.

What each sign-in cannot do

A browser sign-in acts as you, but some things are never approved for an app, whatever access you choose:

  • Managing API keys. keys:write and keys:manage are never granted, so creating, rotating and revoking keys needs an API key that holds keys:manage, or the web app. openemail me rotate rotates the key you are calling with, so it takes an API key.
  • Billing, and the workspaces themselves. Plans, invoices, and creating, switching or deleting a workspace stay in the web app.
  • Your free address. An app is approved for a business workspace, and the personal workspace that holds the free address is never offered, the same rule the API follows.
  • Members and roles, unless the approval covers the whole workspace. members:write and roles:write are dropped from an approval limited to some domains or addresses.

An API key has a limit of its own. ai compose, ai summarize and every openemail mcp command except config go through the MCP server, which takes a browser sign-in, so with a key they stop with exit code 4 and say why.

Verification codes

With a browser sign-in, a few changes ask for a verification code first, as they do in the web app. The CLI asks when it needs one: it emails you a six-digit code, or, when two-factor sign-in is on, asks for a code from your authenticator app or one of your backup codes. Once the code is right the command runs, and that sign-in is not asked again for 60 minutes. An API key is never asked.

CommandAsks for a code
webhooks create, updateAlways
rules create, updateAlways
roles update, deleteAlways
members add, update, removeAlways
members grant-address, revoke-addressAlways
domains delete, delete-addressAlways
audiences deleteFor an audience you created
audiences emptyFor an audience you created that still has contacts
mcp call createRule, setRuleEnabledAlways
mcp call removeDomain, removeDomainAddressAlways
mcp call deleteAudience, emptyAudienceAs the matching audience command does
apiWhen the operation it calls is one of the above
Terminal
$ openemail webhooks create --url https://acme.com/hooks/openemailWe emailed a code to a•••@acme.com.Verification code: 482913Verified. You will not be asked again for 60 minutes.
  • Type r at the prompt to have the email sent again. A wrong code says how many tries are left.
  • After the code is accepted the command runs once more, never twice.
  • --yes confirms a deletion, but never skips a code.
  • Unattended (with --json or --no-input, in CI, or without a terminal) nobody can type the code, so the command stops with exit code 4 and changes nothing.
  • A code allows 5 tries, and after the fifth wrong one the CLI offers a new code. Each sign-in can ask for 5 codes an hour and 20 a day.
  • Ten wrong codes for one sign-in within 24 hours pause its verification. The CLI then says when it resumes and stops with exit code 4 and step_up_paused, without offering another code, and the email explaining it names the app.

Run openemail verify before a script or an AI client does something sensitive. It asks for the code now, and for the next 60 minutes every command on that profile runs without one, openemail mcp call and the local MCP bridge included.

Terminal
openemail verifyopenemail verify --statusopenemail verify --status --jsonopenemail verify --force

The 60 minutes belong to one sign-in. Another profile, or an AI client that signed in on its own, is asked for its own code, and signing out ends them at once. --force asks for a new code and starts a fresh 60 minutes.

Expiry, sign-out and revocation

  • The access token behind a browser sign-in lasts an hour. The CLI refreshes it before it runs out and saves the new one, so you never see it happen.
  • Each refresh token works once. An old one used more than 30 seconds after the CLI replaced it, for example from a copy of config.json on another machine, makes the server revoke that sign-in entirely, so sign in on each machine rather than copying the file.
  • The approval lasts as long as you chose on the approval page. When it ends, or when the app is removed in Account → Connected apps, the CLI can no longer act for you and asks you to run openemail login again.
  • openemail logout revokes a browser sign-in on the server, which removes it from Connected apps, then forgets it on this device, even when the server cannot be reached. --all signs out of every profile.
  • Signing out of an API key only forgets it here. The key keeps working until you revoke it, with openemail keys revoke <id> or in the web app.

Where sign-ins are kept

Everything lives in ~/.openemail, or in the folder OPENEMAIL_CONFIG_DIR names. The folder is readable by you alone (0700), and so is every file in it (0600). Each file is written to a temporary file and renamed into place, so a crash never leaves half of one, and every change is made under a lock file, so commands running side by side never lose a profile.

FileWhat it holds
config.jsonYour profiles: API keys, access and refresh tokens, and which profile is active
temp-mail.jsonThe disposable inboxes this CLI created, with their inbox tokens
update-check.jsonWhen npm was last asked about a new release, and what it said

Tokens and keys are stored as plain text in files only your user can read, so treat the folder as you would an SSH key. A file the CLI cannot make sense of is never read as signed out in silence: it warns once with the path, and keeps a copy beside it (config.json.bak) before writing a new one. A file it cannot read at all, for example because of its permissions, stops the command with an error that names it.

Your inbox,
on your own terms.

Email infrastructure for businesses, AI, agents and personal email. Built for scale, privacy and control. Everything email should have had from day one.

OpenEmail

Email infrastructure for businesses, AI, agents and personal email. Built for scale, privacy and control. Everything email should have had from day one.

© 2026 OpenEmail. All rights reserved.