Skip to the documentation
TypeScript

Configuration

Three ways to build a client, every option, and what it refuses before a request is sent.

Options

nextmail.ts
import Nextmail, { createNextmail, init, nextmail } from '@nextmail/sdk' init({ apiKey: process.env.NEXTMAIL_API_KEY })await nextmail.me.ping() export const billing = createNextmail({ apiKey: process.env.BILLING_API_KEY! }) const pinned = new Nextmail({ apiKey: 'oe_live_…', baseUrl: 'https://api.openemail.uk' }) const quick = new Nextmail('oe_live_…')
Entry pointWhat it gives you
init(options)Configures the shared client and returns it. nextmail is that client from then on, in every module, and anything you leave out is read from the environment.
nextmailThe shared client. Used before init, it builds itself from NEXTMAIL_API_KEY and NEXTMAIL_BASE_URL on the first call.
createNextmail(options)A separate client with the same environment fallback, for a second key beside the shared one, or to build the instance your own module exports. createClient is the same function under the name the envless SDK uses.
new Nextmail(options) or new Nextmail(apiKey)A separate client built from exactly what you pass. It reads no environment, so apiKey is required. Also the default export.
options.ts
init({  apiKey: 'oe_live_…',  baseUrl: 'https://api.openemail.uk',  timeoutMs: 30_000,  maxRetries: 2,  fetch: myFetch,  headers: {},  userAgent: 'billing-service/1.4',  disableUpdateNotice: true,})
OptionDefaultNotes
apiKeyNEXTMAIL_API_KEYRead from the environment by init and createNextmail. Must begin oe_live_ or oe_test_.
baseUrlhttps://api.openemail.ukOr NEXTMAIL_BASE_URL. A trailing slash is trimmed, and init and createNextmail put https:// in front of a bare host, or http:// in front of a host on this machine: localhost, a 127.x.x.x address or ::1. A credential is never sent over plain http to any other host, and 0.0.0.0 or [::] throws when the client is built, because those are addresses a server listens on, not ones to send requests to.
timeoutMs30000Per attempt, not per call. Covers reading the body, not just the headers. 0 disables it.
maxRetries2Extra attempts after the first, on calls that are safe to repeat. Set on the client, not per call.
fetchthe globalBound for you. Pass one for a proxy, a Worker binding or a test double.
headers{}Sent on every request.
userAgentnextmail-sdk/<version>Sent from every runtime except a browser, which does not allow setting it.
disableUpdateNoticefalseSkips the once-per-process check for a newer version on npm. The check only runs when output goes to a terminal, and NEXTMAIL_DISABLE_UPDATE_NOTICE turns it off too.
dangerouslyAllowBrowserfalseLets the client start where window and document exist. Meant for a test harness that defines them, not for a page.

What it refuses before sending

These throw a plain Error from the line that had the wrong value in it, rather than surfacing as a confusing failure on your first send. The message says what was wrong and what to pass instead.

RefusedWhy
No key at allNeither apiKey nor NEXTMAIL_API_KEY was set, so there is nothing to authenticate with.
A session cookie or session tokenOnly oe_live_ and oe_test_ authenticate here, and the API says so too. The check is a prefix and nothing more, so a revoked key still fails on the wire.
A baseUrl that is not an http or https URLNothing else can be fetched, and an unvalidated one would fail later as a raw TypeError from somewhere else entirely.
A browserThe key would be readable by anyone who opens devtools. See the section below.
No fetch anywherePass one as fetch, or run on Node 20+.
An empty or all-dots id on any methodThrown when the method is called. A path segment of dots is removed by every URL parser, so the request would reach a different endpoint.

There is no testMode option and there will not be one. The key scheme is part of the credential rather than a hint, so mode is a property of the key. nextmail.mode reads the prefix and decides nothing.

One client, several keys

Build the client once and share it. A fresh instance per request throws away the fetch binding and the configuration for nothing, and none of the state on it is per-caller.

For the case that would otherwise force one instance per key, such as a job sending on behalf of several workspaces, pass apiKey on the call. It replaces the Authorization header for that request and leaves nothing behind on the client.

per-call-key.ts
await nextmail.emails.send(message) await nextmail.emails.send(message, { apiKey: workspace.apiKey }) await nextmail.threads.list({ folder: 'inbox', apiKey: workspace.apiKey })await nextmail.webhooks.list({ apiKey: workspace.apiKey })

Every method outside tempMail takes it in its last argument, beside signal, and on a list that is the same object as the filters. It is checked before the request is sent, by the same rule the constructor uses, so a typo throws an Error naming { apiKey } on this call rather than a 401 about a credential you then have to go and find. A retried call keeps the key it was given.

signal is an AbortSignal. Aborting it stops the request, and any retry waiting behind it.

nextmail.mode describes the key the client was CONSTRUCTED with and does not follow an override. Once one client serves several keys there is no single mode to report, so read it off the key you passed.

From a browser

The client refuses to start in a browser and throws before any request goes out. A key in a page is a key you have published: it can send mail and read the mailbox for anyone who opens devtools. Call it from a server, a serverless function or a script instead.

Disposable inboxes are the exception. createTempMail() builds a client that carries no API key, so it is safe in a page. It creates inboxes anonymously, and each read sends the inbox token that create returned, or the newer one extend returned, either per call as inboxToken or once as createTempMail({ inboxToken }).

temp-mail.ts
import { createTempMail } from '@nextmail/sdk' const tempMail = createTempMail() const inbox = await tempMail.create()const { items, expiresAt } = await tempMail.listMessages(inbox.id, { inboxToken: inbox.token })

Where you pass dangerouslyAllowBrowser: true anyway, the API allows exactly Content-Type, Authorization and Idempotency-Key through its CORS preflight, so an extra header in headers fails the preflight rather than the request, and what a browser reports for that says nothing useful.

OAuth access tokens

Not shipped yet

Added in 0.0.6. Earlier releases take an API key only.

An app a person connected over OAuth, such as a command line tool or an agent, holds an access token rather than an API key. Pass it as accessToken, either the token itself, or a function that returns it or a promise of it. The function is called once for every call, and that call’s retries reuse what it returned, so renew the token inside it when it is close to expiring and the client never has to be rebuilt.

access-token.ts
import { Nextmail } from '@nextmail/sdk' export const nextmail = new Nextmail({  accessToken: async () => session.freshAccessToken(),}) const me = await nextmail.me.get() if (me.object === 'oauth_token') console.log(me.clientId, me.expiresAt)
CaseWhat happens
apiKey and accessToken together, or neitherThe constructor throws. With neither, the message names NEXTMAIL_API_KEY and NEXTMAIL_ACCESS_TOKEN.
A value that is not a tokenA token is 1 to 512 characters and does not begin oe_, the check isAccessToken makes. A string that fails it throws from the constructor, and a function that resolves to one rejects the call before anything is sent.
NEXTMAIL_ACCESS_TOKENRead by init, createNextmail and the shared nextmail when you pass neither credential and NEXTMAIL_API_KEY is not set, so a key in the environment wins.
A function that throwsThe call rejects with that error, unchanged, and nothing is sent.
A per-call { apiKey }Replaces the token for that one request, and the function is not called.
modeAlways live under a token.
createTempMail()Sends no credential, whatever the environment holds.
me.get() and me.ping()For a token, get answers with object set to oauth_token, id and roleId null, the connected app’s clientId, and expiresAt, when the person’s approval of the app runs out. ping answers with kind set to oauth, keyId null and the clientId. KeyResource and PingResource become unions, so narrow on object or kind before reading id or keyId.

A token acts for a person and reads their mail as they can, so keep it on a server like a key. The client refuses to start in a browser with one too.

Verification codes

Not shipped yet

Before a sensitive change, such as deleting a domain or changing a webhook, the API asks an access token for the verification code the web app would ask the person for. The call throws an NextmailApiError whose isStepUpRequired is true, and nothing was changed. Ask for a code, verify the one the person gives you, then make the call again. An API key is never asked.

step-up.ts
import { NextmailApiError, nextmail } from '@nextmail/sdk' try {  await nextmail.domains.delete(domain.id)} catch (error) {  if (!(error instanceof NextmailApiError) || !error.isStepUpRequired) throw error   const challenge = await nextmail.security.beginStepUp()   const code = await ask(    challenge.method === 'email'      ? `Enter the code we emailed to ${challenge.sentTo}`      : 'Enter the code from your authenticator app, or a backup code',  )   await nextmail.security.verifyStepUp({ code })  await nextmail.domains.delete(domain.id)}
MethodWhat it does
security.stepUpStatus()Whether the app is verified right now (elevated, elevatedUntil), how the next code is checked (method, email or totp), and minutes, the length of the window. It sends nothing, and it does not report a pause.
security.beginStepUp(body?)Opens a challenge. With email a six-digit code goes to the address the person signs in with, and sentTo shows it masked. With totp they read one from their authenticator app or use a backup code. A challenge that is still open and has tries left is reused unless you pass { resend: true }, and a locked or expired one is replaced by a plain call. Each app may open 5 an hour and 20 in 24 hours for each person, and the next throws 429 step_up_throttled.
security.verifyStepUp({ code })Checks the code and unlocks sensitive changes for this app for 60 minutes, until elevatedUntil, over REST and through the MCP tools that make the same changes. After 10 wrong codes in 24 hours from this app, or 20 from all of the person’s apps together, this call and beginStepUp throw 429 step_up_locked with a message that says when verification resumes.

The client never asks for a code or repeats the call by itself, and none of the three methods is retried automatically, because a retry after a lost answer could send a second email or spend a second try. They need no scope, and an API key calling one gets 400 step_up_not_applicable. STEP_UP_ERROR_CODES names every way a verification can fail, and the API errors page says what to do about each.