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 nextmail 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
nextmail loginYou, in the workspace and with the access you approveAsked before a few sensitive changes
nextmail 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. nextmail login registers a new app for this sign-in, named Nextmail CLI on <your computer>, and opens the Nextmail 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
nextmail loginnextmail login --scopes emails:send,threads:readnextmail 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.
  • A browser sign-in needs a person to approve it. With --no-input, in CI, or when neither stdin nor stderr is a terminal, login stops with exit code 2 before it registers anything, and points at --with-token.

Each browser sign-in is its own connected app, listed in Account → Command line with the access you approved, where you can change or remove it. nextmail open cli 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, 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
$ nextmail login --no-browserOpen this link in a browser on any device to sign in:  https://api.nextmail.sh/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.
  • The code can also be piped in on stdin, while stderr shows the link.

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 (nextmail 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
nextmail login --with-token < ~/.config/nextmail/keyecho "$NEXTMAIL_KEY" | nextmail login --with-token --profile cinextmail 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:

  • NEXTMAIL_API_KEY in the environment is used by every command that sees it, instead of the active profile. When it makes a change while a profile is saved, the CLI warns once on stderr.
  • --api-key <key> is used for that one command.

When there is more than one credential, the first of these wins: --api-key, the profile named by --profile, NEXTMAIL_API_KEY, the profile named by NEXTMAIL_PROFILE, then the active profile. A flag always beats the environment.

Profiles

A profile is one saved sign-in, of either kind. The first is called default. Sign more in with --profile, which leaves the active profile as it is unless none is active, and switch between them:

Terminal
nextmail login --profile worknextmail profile listnextmail profile use worknextmail inbox --profile defaultNEXTMAIL_PROFILE=work nextmail statusnextmail profile currentnextmail 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 $(nextmail profile current) works in a script.
  • profile remove <name> is the same as nextmail logout --profile <name>, and a name that is not saved exits with code 5 from either.
  • 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 nextmail 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 NEXTMAIL_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
nextmail login --profile other --base-url https://api.example.comnextmail inbox --profile other
  • A key from NEXTMAIL_API_KEY or --api-key is not a saved profile, so it goes to the origin in --base-url or NEXTMAIL_BASE_URL, or to https://api.nextmail.sh when neither is set.
  • Commands that send no credential follow --base-url and NEXTMAIL_BASE_URL whatever profile is active: temp new, methods that need no key, docs and open.
  • A disposable inbox keeps the API origin that created it, and its inbox token is only ever sent there. temp read, temp watch, temp delete and the temp-mail commands that use a saved token stop with exit code 2 when --base-url or NEXTMAIL_BASE_URL names another origin. A token passed with --inbox-token follows them, like a key passed with --api-key.
  • Plain http is refused for every origin except localhost, a 127.x.x.x address 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. An origin on 0.0.0.0 or [::], where a server listens, stops with exit code 2 whatever the scheme, and the error names the loopback address to use instead.
  • A request path that would leave the API origin, such as nextmail 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. nextmail me rotate rotates the key you are calling with, so it takes an API key.
  • Other workspaces. A sign-in acts for the one workspace you approved it for: workspaces create and workspaces set-active never move it, and workspaces delete reaches only that workspace, and only when you own it.
  • 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 nextmail 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
members grant-domain, revoke-domainAlways
domains delete, delete-addressAlways
domains update-addressWhen it sets --destination
domains add-address-forwards, update-address-forward, delete-address-forward, resend-address-forward-consentAlways
domains set-address-loginAlways
domains create-app-passwordAlways
domains send-mail-app-setupAlways
domains set-dns-zone, sync-dnsAlways
dns-connections deleteAlways
audiences deleteFor an audience you created
app-host setWhen it replaces a host the workspace already has
app-host deleteAlways
forms deleteAlways
encryption publish-keyAlways
exports start, downloadAlways
billing set-pay-as-you-go, set-pay-as-you-go-limit, confirm-pay-as-you-go, start-checkout, open-portal, save-invoice-detailsAlways
workspaces deleteAlways
mcp callWhen the tool is one the API Authentication page lists
apiWhen the operation it calls is one of the above
billing start-add-on-checkout, cancel-add-onAlways
sending resume-streamAlways
automations deleteAlways
Terminal
$ nextmail webhooks create --url https://acme.com/hooks/nextmailWe 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, and twenty within 24 hours across all the apps you connected pause every app’s, while codes entered on the website still work. The CLI then says when verification resumes and stops with exit code 4 and step_up_paused, without offering another code, and the email explaining it names the app.
  • Emptying an audience never asks for a code.

Run nextmail 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, nextmail mcp call and the local MCP bridge included.

Terminal
nextmail verifynextmail verify --statusnextmail verify --status --jsonnextmail 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. An AI client that cannot ask you for a code can be allowed from the website instead: choose Allow changes for 60 minutes in its menu in Account → Connected apps (nextmail open apps).

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. Two commands that renew the same profile at the same moment get the same new pair, so neither signs the other out. 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’s tokens and its verification, and the profile has to sign in again, 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 → Command line, the CLI can no longer act for you and asks you to run nextmail login again.
  • nextmail logout revokes a browser sign-in on the server, which removes it from Account → Command line, then forgets it on this device, even when the server cannot be reached. --profile <name> picks the profile, and a name that is not saved exits with code 5. --all signs out of every profile, and cannot be combined with --profile.
  • Signing out of an API key only forgets it here. The key keeps working until you revoke it, with nextmail keys revoke <id> or in the web app.

Where sign-ins are kept

Everything lives in ~/.nextmail, or in the folder NEXTMAIL_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.