---
title: "openemail.security"
description: "Every method in this namespace: its signature, its parameters, what it returns and an example."
url: "https://openemail.uk/docs/sdk/reference/security"
area: "SDK"
category: "Reference"
---

# openemail.security

Every method in this namespace: its signature, its parameters, what it returns and an example.

## Methods

Verification codes for apps connected with OAuth: check whether sensitive changes are unlocked, ask for a code and verify it. API keys are never asked for one.

### `security.stepUpStatus()`

Check whether an OAuth token is verified for sensitive changes

```ts
stepUpStatus(options?: RequestScope): Promise<StepUpStatusResource>
```

Tells an app connected with OAuth whether it may make sensitive changes right now. The web app asks a person for a verification code before a change that could lock them out or leak mail, and an app acting for that person is asked the same thing. An API key is never asked, so this call is for OAuth access tokens only.

`elevated` is true while a code verified through `verifyStepUp` still holds, and `elevatedUntil` says when that ends, or is null when there is nothing to end. The elevation belongs to this app acting for this person, and it holds for the app's MCP calls as well as for REST: the MCP tools that make the same guarded changes, such as `createRule` and `removeDomain`, run while it lasts. Another app they connected has to verify on its own, and signing in to the web app does not count here. The person can also allow this app from the website, with Allow changes for 60 minutes on the app in Account settings, Connected apps, and `elevated` is then true here as well.

`method` is how the next code would be checked. It is `totp` when the person has two-factor sign-in turned on, and then the code comes from their authenticator app, or is one of their backup codes. Otherwise it is `email`, and `beginStepUp` emails a code to the address they sign in with. `minutes` is how long an elevation lasts once a code is verified, currently 60.

Call it before a batch of sensitive changes to ask for a code once, up front, rather than waiting for the first 403 `step_up_required`.

**Parameters**

- `options.signal` (`AbortSignal`): Cancels the request.
- `options.apiKey` (`string`): Sends this API key instead of the client's credential for this call only. A key is answered 400 `step_up_not_applicable`.

**Returns**

`StepUpStatusResource` with `object` set to `step_up`, `elevated`, `elevatedUntil`, `method` and `minutes`.

**Example**

```ts
const status = await openemail.security.stepUpStatus()

if (!status.elevated) {
    console.log(`Changes need a ${status.method === 'totp' ? 'code from your authenticator app' : 'code from your email'}`)
}
```

**Notes**

- It needs no scope. Any access token the server accepts may ask about itself.
- An API key is refused with 400 `step_up_not_applicable`. Keys are never asked for a code, so there is nothing to check.
- Reading the status changes nothing and sends nothing, so it is safe to call as often as you like.
- It does not report a pause. After 10 wrong codes in 24 hours from this app, or 20 from all of the person's apps together, `beginStepUp` and `verifyStepUp` answer 429 `step_up_locked`, and only their message says when verification resumes.

Also available in: API [`GET /security/step-up`](https://openemail.uk/docs/api/reference/security#get-security-step-up).

### `security.beginStepUp()`

Ask for a verification code

```ts
beginStepUp(body?: StepUpBegin, options?: RequestScope): Promise<StepUpChallengeResource>
```

Opens a verification challenge for an app connected with OAuth. Call it when a request fails with 403 `step_up_required`, or before a batch of sensitive changes, then pass the code the person gives you to `verifyStepUp`.

`method` says where the code comes from. With `email` a six digit code has just gone to the address the person signs in with, `sentTo` names that address with most of it hidden, such as `a•••@example.com`, and the code works for 10 minutes. The email says a connected app asked for the code and shows the name this app registered with, its client ID, the date the person connected it and, when the app was refused one in the last 15 minutes, the change it last asked to make, so they can see which app is asking before they hand the code over. It also tells them the code unlocks more than one change: for 60 minutes the app can make any change that needs a code and that they gave it access to. With `totp` nothing is sent: the person has two-factor sign-in turned on and reads the code from their authenticator app, or uses one of their backup codes.

A challenge that is still open and still has tries left is reused rather than replaced, so calling this twice does not send two emails. `reused` is true when that happened, and the code sent the first time is the one to enter. Pass `resend: true` to close the open challenge and start a new one, which emails a fresh code. A challenge that five wrong codes locked is never reused, and neither is one that expired, so after `step_up_code_expired`, or after the fifth wrong code, a plain `beginStepUp()` starts a fresh challenge without `resend`.

Each app has a budget of its own with each person: 5 new challenges an hour and 20 in 24 hours. Another app never uses it up, and neither does the person working in the web app, whose own budget is 10 codes an hour.

Wrong codes count against the app too. After 10 in 24 hours, across every challenge it opened, verification is paused for this app. After 20 in 24 hours from all the apps the person connected together, it is paused for every one of them, this app included, even if none of those codes came from it, and removing an app does not lift that pause. Codes entered in the web app count toward neither, and the person can still verify there. While a pause lasts, this call and `verifyStepUp` answer 429 `step_up_locked` with a message that says verification is paused until a given time, and no code is sent or checked before then.

An API key is never asked for a code, so this call is for OAuth access tokens only.

**Parameters**

- `body.resend` (`boolean`): Closes the open challenge and starts a new one, emailing a fresh code when the method is `email`. Leave it out to reuse a challenge that is still open and still has tries left. A locked or expired challenge is replaced either way, and every new challenge counts toward the app's 5 an hour and 20 in 24 hours.
- `options.signal` (`AbortSignal`): Cancels the request.
- `options.apiKey` (`string`): Sends this API key instead of the client's credential for this call only. A key is answered 400 `step_up_not_applicable`.

**Returns**

`StepUpChallengeResource` with `object` set to `step_up_challenge`, `method`, `reused`, and `sentTo`, the masked address, when the code went out by email.

**Example**

```ts
const challenge = await openemail.security.beginStepUp()

console.log(challenge.method === 'email'
    ? `We emailed a code to ${challenge.sentTo}.`
    : 'Enter the code from your authenticator app, or a backup code.')

const again = await openemail.security.beginStepUp({ resend: true })

console.log(again.reused)
```

**Notes**

- It needs no scope. An API key is refused with 400 `step_up_not_applicable`.
- An app can open 5 challenges an hour and 20 in 24 hours for each person. The next is refused with 429 `step_up_throttled`, and the message says when to try again. A reused challenge does not count, and neither does one whose email could not be delivered. Other apps have budgets of their own, and so does the web app, which allows 10 an hour.
- After 10 wrong codes in 24 hours this app is paused, and after 20 from all of the person's apps together every app is: 429 `step_up_locked`, with a message that says verification is paused until a given time. `resend: true` lifts neither pause. The person gets one email about each.
- When the email cannot be delivered the answer is 503 `step_up_undeliverable` and no challenge is left open. Try again in a moment.
- Not retried automatically, because a retry after a lost response could send a second email.

Also available in: API [`POST /security/step-up`](https://openemail.uk/docs/api/reference/security#post-security-step-up).

### `security.verifyStepUp()`

Verify a code and unlock sensitive changes

```ts
verifyStepUp(body: StepUpVerify, options?: RequestScope): Promise<StepUpVerifiedResource>
```

Checks the code the person entered against the challenge `beginStepUp` opened, and on a match lets this app make sensitive changes for them until `elevatedUntil`, 60 minutes from now. Replay the request that failed with 403 `step_up_required` once this resolves.

The verification belongs to this app acting for this person. It unlocks the 16 changes that ask for a code: `webhooks.create`, `webhooks.update`, `rules.create`, `rules.update`, `roles.update`, `roles.delete`, `members.add`, `members.update`, `members.remove`, `members.grantAddress`, `members.revokeAddress`, `domains.delete`, `domains.deleteAddress`, `appHost.delete`, `audiences.delete` and `forms.delete`. Each still needs the scope it always needs. It holds for the MCP tools that make the same changes as well (`createRule`, `setRuleEnabled`, `removeDomain`, `removeDomainAddress`, `removeAppHost`, `deleteForm`, and `deleteAudience` on any audience but the default one), which answer `Refused (step_up_required)` while the app is not verified. Nothing else asks for a code, `audiences.empty` and the `emptyAudience` tool included. Another app the person connected verifies on its own.

The code is the one emailed to them when the method is `email`, or a code from their authenticator app, or one of their backup codes, when it is `totp`. Spaces are ignored, so a code copied as `123 456` works. An authenticator code is accepted only once: entering one that was already accepted, by any app or in the web app, counts as a wrong code, so wait for the next one.

Open a challenge with `beginStepUp` first, even when the code comes from an authenticator app, because the code is checked against that challenge. A challenge allows five tries. The fifth wrong code locks it, and a locked challenge is never reused, so the next `beginStepUp()` starts a fresh one without `resend`.

Wrong codes also count against this app for 24 hours, across every challenge, and a correct code does not clear them. The tenth in 24 hours pauses verification for this app. They count toward a second limit as well, shared by every app the person connected: the twentieth wrong code in 24 hours from all of those apps together pauses verification for every one of them, this app included, even if none of those codes came from it. Removing an app from the person's connected apps does not lift that pause. Codes entered in the web app count toward neither limit, and the person can still verify there during either pause. While a pause lasts, this call and `beginStepUp` answer 429 `step_up_locked` with a message that says verification is paused until a given time, and until then even a correct code is refused.

An app that has no way to ask the person for a code, such as an MCP connector that only calls tools, does not need this call: the person can choose Allow changes for 60 minutes on the app in Account settings, Connected apps, on the website, which lets it make the same changes for 60 minutes. They can end that early from the same menu.

An API key is never asked for a code, so this call is for OAuth access tokens only.

**Parameters**

- `body.code` (`string`, required): The code the person entered: six digits from the email or their authenticator app, or one of their backup codes. 6 to 12 characters once surrounding spaces are trimmed.
- `options.signal` (`AbortSignal`): Cancels the request.
- `options.apiKey` (`string`): Sends this API key instead of the client's credential for this call only. A key is answered 400 `step_up_not_applicable`.

**Returns**

`StepUpVerifiedResource` with `object` set to `step_up`, `elevated` set to true and `elevatedUntil`, 60 minutes from now.

**Example**

```ts
import { OpenEmailApiError, openemail } from '@openemail/sdk'

const update = () => openemail.webhooks.update('whe_3f9c2a7b1e4d8f60a5c7b92d', { enabled: false })

try {
    await update()
}

catch (error) {
    if (!(error instanceof OpenEmailApiError) || !error.isStepUpRequired) throw error

    await openemail.security.beginStepUp()

    const verified = await openemail.security.verifyStepUp({ code: await askForCode() })

    console.log(verified.elevatedUntil)

    await update()
}
```

**Notes**

- It needs no scope. An API key is refused with 400 `step_up_not_applicable`.
- A wrong code is 400 `step_up_code_invalid` on `code`. The message says how many tries are left: the fewest of the tries left on this challenge, the wrong codes left before this app is paused and the wrong codes left before every app is paused.
- The fifth wrong code on a challenge, and every code sent after it, is 429 `step_up_locked`. Ask for a new code with `beginStepUp()`, which starts a fresh challenge without `resend`.
- The tenth wrong code from this app in 24 hours is 429 `step_up_locked` as well, and so is the twentieth from all of the person's apps together, and every call while either pause lasts. The message says verification is paused until a given time, which is how to tell a pause from a locked challenge, and it reads the same for both pauses. The person gets one email about each pause. The one for this app names it. The one for every app says codes from all the apps they connected are paused, names the app the last wrong code came from, and says codes entered on the website still work.
- An expired code, or no open challenge at all, is 400 `step_up_code_expired`. Ask for a new code with `beginStepUp()`.
- A code shorter than 6 or longer than 12 characters, or a body with any field besides `code`, is refused before it is checked and does not use up a try.
- Not retried automatically. A retry after a lost response would spend a second attempt on a challenge the first one may already have closed.

Also available in: API [`POST /security/step-up/verify`](https://openemail.uk/docs/api/reference/security#post-security-step-up-verify).
