---
title: "Configuration"
description: "Three ways to build a client, every option, and what it refuses before a request is sent."
url: "https://openemail.uk/docs/sdk/configuration"
area: "SDK"
category: "Getting started"
---

# Configuration

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

## Options

**openemail.ts**

```
import OpenEmail, { createOpenEmail, init, openemail } from '@openemail/sdk'

init({ apiKey: process.env.OPENEMAIL_API_KEY })
await openemail.me.ping()

export const billing = createOpenEmail({ apiKey: process.env.BILLING_API_KEY! })

const pinned = new OpenEmail({ apiKey: 'oe_live_…', baseUrl: 'https://api.openemail.uk' })

const quick = new OpenEmail('oe_live_…')
```

| Entry point | What it gives you |
| --- | --- |
| `init(options)` | Configures the shared client and returns it. `openemail` is that client from then on, in every module, and anything you leave out is read from the environment. |
| `openemail` | The shared client. Used before `init`, it builds itself from `OPENEMAIL_API_KEY` and `OPENEMAIL_BASE_URL` on the first call. |
| `createOpenEmail(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 OpenEmail(options)` or `new OpenEmail(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,
})
```

| Option | Default | Notes |
| --- | --- | --- |
| `apiKey` | `OPENEMAIL_API_KEY` | Read from the environment by `init` and `createOpenEmail`. Must begin `oe_live_` or `oe_test_`. |
| `baseUrl` | `https://api.openemail.uk` | Or `OPENEMAIL_BASE_URL`. A trailing slash is trimmed, and `init` and `createOpenEmail` put `https://` in front of a bare host, or `http://` in front of localhost. |
| `timeoutMs` | 30000 | Per attempt, not per call. Covers reading the body, not just the headers. 0 disables it. |
| `maxRetries` | 2 | Extra attempts after the first, on calls that are safe to repeat. Set on the client, not per call. |
| `fetch` | the global | Bound for you. Pass one for a proxy, a Worker binding or a test double. |
| `headers` | `{}` | Sent on every request. |
| `userAgent` | `openemail-sdk/<version>` | Sent from every runtime except a browser, which does not allow setting it. |
| `disableUpdateNotice` | false | Skips the once-per-process check for a newer version on npm. The check only runs when output goes to a terminal, and `OPENEMAIL_DISABLE_UPDATE_NOTICE` turns it off too. |
| `dangerouslyAllowBrowser` | false | Lets 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.

| Refused | Why |
| --- | --- |
| No key at all | Neither `apiKey` nor `OPENEMAIL_API_KEY` was set, so there is nothing to authenticate with. |
| A session cookie or session token | Only `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 URL | Nothing else can be fetched, and an unvalidated one would fail later as a raw `TypeError` from somewhere else entirely. |
| A browser | The key would be readable by anyone who opens devtools. See the section below. |
| No `fetch` anywhere | Pass one as `fetch`, or run on Node 20+. |
| An empty or all-dots id on any method | Thrown 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. `openemail.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 openemail.emails.send(message)

await openemail.emails.send(message, { apiKey: workspace.apiKey })

await openemail.threads.list({ folder: 'inbox', apiKey: workspace.apiKey })
await openemail.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.

> `openemail.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 token `create` returned, either per call as `inboxToken` or once as `createTempMail({ inboxToken })`.

**temp-mail.ts**

```
import { createTempMail } from '@openemail/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.
