---
title: "Security"
description: "Every operation in this group: what it accepts, what it returns and the errors it can answer with."
url: "https://nextmail.sh/docs/api/reference/security"
area: "API"
category: "Reference"
---

# Security

Every operation in this group: what it accepts, what it returns and the errors it can answer with.

## Operations

Verification codes for OAuth access tokens. A few operations change who can reach the workspace or where its mail goes, and an app acting for a person has to confirm who they are before it does them, exactly as the app does: ask for a code, send the code the person typed, and repeat the call. API keys are never asked for a code.

### `GET /security/step-up`

Whether this access token is verified

Needs no scope, and is for OAuth access tokens only: an API key is refused with 400 `step_up_not_applicable`, because a key is never asked for a code.

Some operations change who can reach the workspace or where its mail goes, and the app asks the person to confirm who they are before it does them. An app acting for that person is held to the same rule: called with an OAuth access token, each of those operations answers 403 `step_up_required` until the token is verified, and one verification lasts 60 minutes. `method` says which kind of code to ask the person for.

- Needs an API key or an OAuth access token, and no scope.

**Returns**

- `200` `StepUpStatus`: Whether the token is verified, until when, and which kind of code it takes.

**Errors**

- `400`: `step_up_not_applicable`: the call was made with an API key, which is never asked for a code.
- The errors every operation can return: `401`, `403`, `404`, `422`, `429`, `500`, described in the [error catalog](https://nextmail.sh/docs/api/errors).

Also available in: TypeScript [`nextmail.security.stepUpStatus()`](https://nextmail.sh/docs/sdk/reference/security#stepUpStatus); Python [`security.step_up_status()`](https://nextmail.sh/docs/python/reference/security#stepUpStatus); Ruby [`security.step_up_status`](https://nextmail.sh/docs/ruby/reference/security#stepUpStatus); PHP [`security->stepUpStatus`](https://nextmail.sh/docs/php/reference/security#stepUpStatus); Go [`Security.StepUpStatus`](https://nextmail.sh/docs/go/reference/security#stepUpStatus); Java [`security().stepUpStatus`](https://nextmail.sh/docs/java/reference/security#stepUpStatus); C# [`Security.StepUpStatusAsync`](https://nextmail.sh/docs/csharp/reference/security#stepUpStatus).

### `POST /security/step-up`

Ask for a verification code

Needs no scope, and is for OAuth access tokens only. Starts a verification for the person the token acts for.

With `method` `email`, a six-digit code goes to the address they sign in with and works for 10 minutes. The email names the app and, when a call from it was refused for want of a code in the last 15 minutes, the last thing it asked to do. It also tells them that the code is not tied to that one call: for 60 minutes it lets the app make every change that needs a code, within the access they gave it. With `totp`, nothing is sent: they read the code off their authenticator app, or use one of their backup codes. Either way, send what they type to `POST /security/step-up/verify`.

A code already waiting is reused rather than sent again, which `reused` reports; pass `resend: true` to send a fresh one. A code that took 5 wrong tries is never reused, so a call without `resend` then starts a fresh one.

Each app has its own allowance for each person: 5 codes an hour and 20 in 24 hours. It is separate from every other app's and from the 10 an hour the person has on the website. After 10 wrong codes for one app in 24 hours, verification for that app is paused, and after 20 wrong codes across all of the person's apps it is paused for every app. Either way this answers 429 `step_up_locked` until the time its message gives.

- Needs an API key or an OAuth access token, and no scope.

**Request body**

- `resend` (`boolean`): Send a new code even though one is still waiting, and stop the waiting one from working. Left out, a waiting code that still has tries left is reused and nothing is sent.
- `method` (`string`, one of `"totp"`, `"email"`): `email` sends a code to the address the person signs in with even when they have an authenticator app, for when they would rather use their inbox. `totp` asks for the authenticator code and only applies when two-factor sign-in is on. Left out, the authenticator is asked for when two-factor sign-in is on and an email code otherwise. A waiting code of the other kind is replaced.

**Returns**

- `200` `StepUpChallenge`: How the person gets their code, and where it went.

**Errors**

- `400`: `step_up_not_applicable`: the call was made with an API key, which is never asked for a code.
- `429`: `step_up_throttled`: this app has asked for too many codes for this person. Each app has its own allowance of 5 codes an hour and 20 in 24 hours, and the message says when the next one can be asked for. It is separate from every other app's and from the 10 an hour the person has on the website, so one app never uses those up. `step_up_locked`: 10 wrong codes were sent for this app in the last 24 hours, so verification is paused for it. No code can be asked for or checked until the time the message gives after `paused until`, which is when the oldest of those wrong codes is 24 hours old. The same answer comes once the person's apps together have sent 20 wrong codes in 24 hours: verification is then paused for every app they connected, and removing an app does not lift that pause. Codes entered on the website are not affected. The person is told by email. `rate_limited`: the key or app made more than 600 requests in 60 seconds. Wait for the seconds in `Retry-After`.
- `503`: `step_up_undeliverable`: the email with the code could not be sent just now. Try again in a moment.
- The errors every operation can return: `401`, `403`, `404`, `422`, `500`, described in the [error catalog](https://nextmail.sh/docs/api/errors).

Also available in: TypeScript [`nextmail.security.beginStepUp()`](https://nextmail.sh/docs/sdk/reference/security#beginStepUp); Python [`security.begin_step_up()`](https://nextmail.sh/docs/python/reference/security#beginStepUp); Ruby [`security.begin_step_up`](https://nextmail.sh/docs/ruby/reference/security#beginStepUp); PHP [`security->beginStepUp`](https://nextmail.sh/docs/php/reference/security#beginStepUp); Go [`Security.BeginStepUp`](https://nextmail.sh/docs/go/reference/security#beginStepUp); Java [`security().beginStepUp`](https://nextmail.sh/docs/java/reference/security#beginStepUp); C# [`Security.BeginStepUpAsync`](https://nextmail.sh/docs/csharp/reference/security#beginStepUp).

### `POST /security/step-up/verify`

Send the verification code

Needs no scope, and is for OAuth access tokens only. Checks the code the person typed against the one `POST /security/step-up` started, and on a match verifies this token for 60 minutes. Every operation that answered `step_up_required` works until `elevatedUntil`, and so do the app's MCP tools that need a code.

A code takes at most 5 tries, and an authenticator code is accepted only once. Wrong codes also add up for the app: after 10 in 24 hours, verification for that app is paused (after 20 across all of the person's apps, for every app), even a correct code is refused until the time the 429 message gives, and the person is told by email. The verification belongs to the app the token was issued to, by its client ID, so a second app verifies separately.

- Needs an API key or an OAuth access token, and no scope.

**Request body**

- `code` (`string`, required, 6 to 12 characters): The six-digit code from the email or the authenticator app, or one of the backup codes, such as `abcde-12345`. Spaces are ignored. A backup code works once.

**Returns**

- `200` `StepUpVerification`: Verified, and until when.

**Errors**

- `400`: `step_up_code_invalid`: the code did not match, and the message says how many tries are left, counting down to whichever runs out first: this code's 5 tries, the 10 wrong codes the app may send in 24 hours, or the 20 wrong codes all of the person's apps together may send in 24 hours. An authenticator code is accepted once, so sending it again counts as a wrong code. `step_up_code_expired`: there is no code waiting, or it ran out after 10 minutes. Ask for a new one with `POST /security/step-up`. `step_up_not_applicable`: the call was made with an API key.
- `429`: `step_up_locked`: 5 wrong codes were sent for this code, so it no longer works. Ask for a new one with `POST /security/step-up`. A code that ran out of tries is never reused, so that call starts a fresh one even without `resend`. `step_up_locked`: 10 wrong codes were sent for this app in the last 24 hours, so verification is paused for it. No code can be asked for or checked until the time the message gives after `paused until`, which is when the oldest of those wrong codes is 24 hours old. The same answer comes once the person's apps together have sent 20 wrong codes in 24 hours: verification is then paused for every app they connected, and removing an app does not lift that pause. Codes entered on the website are not affected. The person is told by email. `rate_limited`: the key or app made more than 600 requests in 60 seconds. Wait for the seconds in `Retry-After`.
- The errors every operation can return: `401`, `403`, `404`, `422`, `500`, described in the [error catalog](https://nextmail.sh/docs/api/errors).

Also available in: TypeScript [`nextmail.security.verifyStepUp()`](https://nextmail.sh/docs/sdk/reference/security#verifyStepUp); Python [`security.verify_step_up()`](https://nextmail.sh/docs/python/reference/security#verifyStepUp); Ruby [`security.verify_step_up`](https://nextmail.sh/docs/ruby/reference/security#verifyStepUp); PHP [`security->verifyStepUp`](https://nextmail.sh/docs/php/reference/security#verifyStepUp); Go [`Security.VerifyStepUp`](https://nextmail.sh/docs/go/reference/security#verifyStepUp); Java [`security().verifyStepUp`](https://nextmail.sh/docs/java/reference/security#verifyStepUp); C# [`Security.VerifyStepUpAsync`](https://nextmail.sh/docs/csharp/reference/security#verifyStepUp).

### `GET /security/events`

Read the workspace security log

The changes people made to who can reach the workspace, newest first: invitations sent and withdrawn, members added and removed, roles created, changed and deleted, the two-factor requirement, passwords set for an address and directory sync. Each one names who did it. Nothing is ever removed from it. Paging is keyset: pass `nextCursor` back as `cursor` while `hasMore` is true.

Requires the `members:read` scope.

- Scopes: `members:read`.

**Query parameters**

- `limit` (`integer`, at least 1, at most 100, default `25`): Rows per page, 1 to 100.
- `cursor` (`string`): The previous page's `nextCursor`, passed back as it came. It is opaque: it holds where the last row sat in this list's order, so a row deleted or edited between pages never breaks the walk, and the next page starts at the first row that sorts after it. A value this list did not hand out is a 400 `invalid_cursor`.

**Returns**

- `200` `SecurityEventList`: A page of the log.

**Errors**

- The errors every operation can return: `400`, `401`, `403`, `404`, `422`, `429`, `500`, described in the [error catalog](https://nextmail.sh/docs/api/errors).

Also available in: TypeScript [`nextmail.security.listEvents()`](https://nextmail.sh/docs/sdk/reference/security#listEvents), [`nextmail.security.listAllEvents()`](https://nextmail.sh/docs/sdk/reference/security#listAllEvents), [`nextmail.security.iterateEvents()`](https://nextmail.sh/docs/sdk/reference/security#iterateEvents); Python [`security.list_events()`](https://nextmail.sh/docs/python/reference/security#listEvents), [`security.list_all_events()`](https://nextmail.sh/docs/python/reference/security#listAllEvents), [`security.iterate_events()`](https://nextmail.sh/docs/python/reference/security#iterateEvents); Ruby [`security.list_events`](https://nextmail.sh/docs/ruby/reference/security#listEvents), [`security.list_all_events`](https://nextmail.sh/docs/ruby/reference/security#listAllEvents), [`security.iterate_events`](https://nextmail.sh/docs/ruby/reference/security#iterateEvents); PHP [`security->listEvents`](https://nextmail.sh/docs/php/reference/security#listEvents), [`security->listAllEvents`](https://nextmail.sh/docs/php/reference/security#listAllEvents), [`security->iterateEvents`](https://nextmail.sh/docs/php/reference/security#iterateEvents); Go [`Security.ListEvents`](https://nextmail.sh/docs/go/reference/security#listEvents), [`Security.ListAllEvents`](https://nextmail.sh/docs/go/reference/security#listAllEvents), [`Security.IterateEvents`](https://nextmail.sh/docs/go/reference/security#iterateEvents); Java [`security().listEvents`](https://nextmail.sh/docs/java/reference/security#listEvents), [`security().listAllEvents`](https://nextmail.sh/docs/java/reference/security#listAllEvents), [`security().iterateEvents`](https://nextmail.sh/docs/java/reference/security#iterateEvents); C# [`Security.ListEventsAsync`](https://nextmail.sh/docs/csharp/reference/security#listEvents), [`Security.ListAllEventsAsync`](https://nextmail.sh/docs/csharp/reference/security#listAllEvents), [`Security.IterateEventsAsync`](https://nextmail.sh/docs/csharp/reference/security#iterateEvents); CLI [`nextmail security list-events`](https://nextmail.sh/docs/cli/reference/security#security-list-events); MCP [`listWorkspaceSecurityEvents`](https://nextmail.sh/docs/mcp/tools/workspace#listWorkspaceSecurityEvents).

### Objects

#### `SecurityEvent`

`object`

- `object` (`string`, required, one of `"security_event"`)
- `id` (`string`, required)
- `scope` (`string`, required, one of `"account"`, `"workspace"`): `account` for the person's own log, `workspace` for the workspace's.
- `kind` (`string`, required, one of `"sign-in"`, `"session-revoked"`, `"sessions-revoked"`, `"password-changed"`, `"password-reset"`, `"two-factor-enabled"`, `"two-factor-disabled"`, `"passkey-added"`, `"passkey-removed"`, `"member-added"`, `"member-removed"`, `"member-role-changed"`, `"invitation-sent"`, `"invitation-revoked"`, `"role-created"`, `"role-changed"`, `"role-deleted"`, `"two-factor-requirement-changed"`, `"address-password-set"`, `"address-password-removed"`, `"directory-sync-connected"`, `"directory-sync-disconnected"`, `"directory-sync-token-rotated"`, `"directory-sync-changed"`, `"connector-connected"`, `"connector-disconnected"`, `"connector-changed"`, `"ownership-offered"`, `"ownership-transferred"`)
- `at` (`string`, required, format `date-time`)
- `actor` (`object`, required, nullable): Who made a workspace change, or null for the person's own log.
  - `name` (`string`, required, nullable)
  - `email` (`string`, required, nullable)
- `member` (`object`, required, nullable): The person a workspace change was about.
  - `name` (`string`, required, nullable)
  - `email` (`string`, required, nullable)
- `device` (`object`, required, nullable): The device it happened on, when one was known.
  - `kind` (`string`, required, one of `"browser"`, `"app"`, `"unknown"`): `browser`, `app` for the Nextmail phone app, or `unknown`.
  - `browser` (`string`, required, nullable): Such as `Chrome`.
  - `os` (`string`, required, nullable): Such as `macOS` or `iOS`.
  - `name` (`string`, required, nullable): The name of the phone, when the phone app reported one.
  - `appVersion` (`string`, required, nullable)
- `authMethod` (`string`, required, nullable, one of `"password"`, `"totp"`, `"backup-code"`, `"passkey"`, `"email-link"`, `"handoff"`): How a sign-in was made.
- `country` (`string`, required, nullable)
- `city` (`string`, required, nullable)
- `detail` (`SecurityEventDetail`, required)

#### `SecurityEventDetail`

`object`

What the event was about. Only the fields that apply to its `kind` are present.

- `byKey` (`boolean`): True when an API key made the change.
- `byDirectory` (`boolean`): True when directory sync made the change.
- `role` (`string`, nullable)
- `previousRole` (`string`, nullable)
- `passkey` (`string`, nullable): The name of the passkey.
- `device` (`string`, nullable): The device that was signed out, as an English label.
- `deviceInfo` (`object`, nullable): The same device in parts, so a client can word it itself. Absent on entries recorded before it was added.
  - `kind` (`string`, required, one of `"browser"`, `"app"`, `"unknown"`): `browser`, `app` for the Nextmail phone app, or `unknown`.
  - `browser` (`string`, required, nullable): Such as `Chrome`.
  - `os` (`string`, required, nullable): Such as `macOS` or `iOS`.
  - `name` (`string`, required, nullable): The name of the phone, when the phone app reported one.
  - `appVersion` (`string`, required, nullable)
- `sessions` (`integer`): How many devices were signed out.
- `apps` (`integer`): How many connected apps were removed.
- `newDevice` (`boolean`): True for a sign-in from a device not seen before.
- `address` (`string`, nullable)
- `required` (`boolean`)
- `enabled` (`boolean`)
- `connector` (`string`, nullable): The name of the app a connector entry is about.
- `access` (`string`, nullable): What the connector lets the assistant do, `read` or `full`.
- `paused` (`boolean`): Whether the connector is paused.

#### `SecurityEventList`

`object`

- `object` (`string`, one of `"list"`)
- `data` (`SecurityEvent[]`)
- `hasMore` (`boolean`): True when another page follows. Pass `nextCursor` back as `cursor` to read it.
- `nextCursor` (`string`, nullable): An opaque cursor for the next page, or null on the last page. Pass it back unchanged.

#### `StepUpChallenge`

`object`

- `object` (`string`, one of `"step_up_challenge"`)
- `method` (`string`, one of `"totp"`, `"email"`): `totp` when the person has two-factor authentication turned on: the code comes from their authenticator app, or is one of their backup codes. `email` otherwise: a six-digit code that works for 10 minutes is emailed to the address they sign in with.
- `sentTo` (`string`): Only when `method` is `email`: the address the code went to, masked to its first character and its domain, such as `a•••@example.com`.
- `reused` (`boolean`): True when a code sent earlier is still waiting and nothing new was sent. Pass `resend: true` to send a fresh one. A code that took 5 wrong tries is never reused, so the next call sends a fresh one without `resend`.

#### `StepUpStatus`

`object`

- `object` (`string`, one of `"step_up"`)
- `elevated` (`boolean`): Whether this access token may call the operations that need a verification code right now.
- `elevatedUntil` (`string`, nullable, format `date-time`): When the current verification runs out. Null when there is none.
- `method` (`string`, one of `"totp"`, `"email"`): `totp` when the person has two-factor authentication turned on: the code comes from their authenticator app, or is one of their backup codes. `email` otherwise: a six-digit code that works for 10 minutes is emailed to the address they sign in with.
- `minutes` (`integer`, one of `60`): How long one verification lasts, in minutes.

#### `StepUpVerification`

`object`

- `object` (`string`, one of `"step_up"`)
- `elevated` (`boolean`, one of `true`)
- `elevatedUntil` (`string`, format `date-time`): When this verification runs out, 60 minutes from now.
