# Users · Security

Part of AppEngine API Documentation. Full index: https://appengine.appmint.io/documentation.md
## GET /profile/security/devices

**List my devices**

`operationId: SecurityController_listDevices`

The devices that have signed in to the caller's account, with when each was last seen. The read behind a "where you're signed in" screen, and the first place a user looks after a suspicious-login alert.

#### Signature

```http
GET /profile/security/devices () -> The account's devices
```

#### Access

Requires a bearer JWT (`Authorization: Bearer <token>`).

#### Errors

Plus the standard platform errors: `401`, `403`, `429`, `500`.

#### See also

- `GET /profile/security/login-history`

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `orgid` | header | string | yes | Organization (tenant) identifier. Every request is scoped to this org; data from other orgs is never visible. Issued with your API credentials. |

### Responses

| Status | Meaning |
| --- | --- |
| `200` | The account's devices |
| `401` | Authentication failed: Invalid or expired token — The `Authorization` header is missing, malformed, or the JWT has expired. |
| `403` | You do not have permission to perform this action — The caller is authenticated but lacks the role required by the endpoint, or is acting on another org. |
| `429` | Too Many Requests — More than 100,000 requests from one IP within 5 minutes (configurable per deployment). CORS preflights and requests from inside the platform cluster are not counted. The limiter answers before the error filter, so the body is `{ statusCode, error, message }` with no `path`, `method` or `timeStamp`; the `RateLimit-*` response headers say when the window resets. |
| `500` | An unexpected error occurred. Our team has been notified. — An unhandled server-side failure. |

## GET /profile/security/devices/current

**Get the current device**

`operationId: SecurityController_getCurrentDevice`

Identifies the device making this request, so a device list can mark which entry is "this one" and avoid the user revoking their own session by mistake.

#### Signature

```http
GET /profile/security/devices/current () -> The current device
```

#### Access

Requires a bearer JWT (`Authorization: Bearer <token>`).

#### Errors

Plus the standard platform errors: `401`, `403`, `429`, `500`.

#### See also

- `GET /profile/security/devices`

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `orgid` | header | string | yes | Organization (tenant) identifier. Every request is scoped to this org; data from other orgs is never visible. Issued with your API credentials. |

### Responses

| Status | Meaning |
| --- | --- |
| `200` | The current device |
| `401` | Authentication failed: Invalid or expired token — The `Authorization` header is missing, malformed, or the JWT has expired. |
| `403` | You do not have permission to perform this action — The caller is authenticated but lacks the role required by the endpoint, or is acting on another org. |
| `429` | Too Many Requests — More than 100,000 requests from one IP within 5 minutes (configurable per deployment). CORS preflights and requests from inside the platform cluster are not counted. The limiter answers before the error filter, so the body is `{ statusCode, error, message }` with no `path`, `method` or `timeStamp`; the `RateLimit-*` response headers say when the window resets. |
| `500` | An unexpected error occurred. Our team has been notified. — An unhandled server-side failure. |

## POST /profile/security/devices/{deviceId}/trust

**Trust a device**

`operationId: SecurityController_trustDevice`

Marks a device as trusted, which typically lets it skip two-factor prompts for a period.

**This deliberately weakens a protection.** Trust only a device the user controls, and keep `durationDays` short — a trusted device on shared hardware defeats the second factor entirely.

#### Signature

```http
POST /profile/security/devices/{deviceId}/trust (deviceId: string, body) -> The trusted device
```

#### Access

Requires a bearer JWT (`Authorization: Bearer <token>`).

#### Notes

- Never offer this on a public or shared machine.

#### Errors

Plus the standard platform errors: `401`, `403`, `429`, `500`.

#### See also

- `POST /profile/security/devices/{deviceId}/block`

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `orgid` | header | string | yes | Organization (tenant) identifier. Every request is scoped to this org; data from other orgs is never visible. Issued with your API credentials. |
| `deviceId` | path | string | yes | Device id. |

### Request body

How long to trust it.

```json
{
  "durationDays": 30
}
```

### Responses

| Status | Meaning |
| --- | --- |
| `201` | The trusted device |
| `401` | Authentication failed: Invalid or expired token — The `Authorization` header is missing, malformed, or the JWT has expired. |
| `403` | You do not have permission to perform this action — The caller is authenticated but lacks the role required by the endpoint, or is acting on another org. |
| `429` | Too Many Requests — More than 100,000 requests from one IP within 5 minutes (configurable per deployment). CORS preflights and requests from inside the platform cluster are not counted. The limiter answers before the error filter, so the body is `{ statusCode, error, message }` with no `path`, `method` or `timeStamp`; the `RateLimit-*` response headers say when the window resets. |
| `500` | An unexpected error occurred. Our team has been notified. — An unhandled server-side failure. |

## POST /profile/security/devices/{deviceId}/block

**Block a device**

`operationId: SecurityController_blockDevice`

Blocks a device from signing in — the immediate response to an unrecognised entry in the device list. Blocking does not end an existing session on its own, so sign the account out as well if the session may still be live.

#### Signature

```http
POST /profile/security/devices/{deviceId}/block (deviceId: string, body) -> The blocked device
```

#### Access

Requires a bearer JWT (`Authorization: Bearer <token>`).

#### Notes

- Pair with a sign-out and a password change if the device may have an active session.

#### Errors

Plus the standard platform errors: `401`, `403`, `429`, `500`.

#### See also

- `POST /signout`
- `POST /password/change`

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `orgid` | header | string | yes | Organization (tenant) identifier. Every request is scoped to this org; data from other orgs is never visible. Issued with your API credentials. |
| `deviceId` | path | string | yes | Device id. |

### Request body

Why it was blocked.

```json
{
  "reason": "Unrecognised device"
}
```

### Responses

| Status | Meaning |
| --- | --- |
| `201` | The blocked device |
| `401` | Authentication failed: Invalid or expired token — The `Authorization` header is missing, malformed, or the JWT has expired. |
| `403` | You do not have permission to perform this action — The caller is authenticated but lacks the role required by the endpoint, or is acting on another org. |
| `429` | Too Many Requests — More than 100,000 requests from one IP within 5 minutes (configurable per deployment). CORS preflights and requests from inside the platform cluster are not counted. The limiter answers before the error filter, so the body is `{ statusCode, error, message }` with no `path`, `method` or `timeStamp`; the `RateLimit-*` response headers say when the window resets. |
| `500` | An unexpected error occurred. Our team has been notified. — An unhandled server-side failure. |

## DELETE /profile/security/devices/{deviceId}

**Remove a device**

`operationId: SecurityController_removeDevice`

Removes a device from the account. Unlike blocking, this forgets it — the same device signing in again is treated as new, which is usually what you want after replacing hardware.

#### Signature

```http
DELETE /profile/security/devices/{deviceId} (deviceId: string) -> Removal result
```

#### Access

Requires a bearer JWT (`Authorization: Bearer <token>`).

#### Errors

Plus the standard platform errors: `401`, `403`, `429`, `500`.

#### See also

- `POST /profile/security/devices/{deviceId}/block`

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `orgid` | header | string | yes | Organization (tenant) identifier. Every request is scoped to this org; data from other orgs is never visible. Issued with your API credentials. |
| `deviceId` | path | string | yes | Device id. |

### Responses

| Status | Meaning |
| --- | --- |
| `200` | Removal result |
| `401` | Authentication failed: Invalid or expired token — The `Authorization` header is missing, malformed, or the JWT has expired. |
| `403` | You do not have permission to perform this action — The caller is authenticated but lacks the role required by the endpoint, or is acting on another org. |
| `429` | Too Many Requests — More than 100,000 requests from one IP within 5 minutes (configurable per deployment). CORS preflights and requests from inside the platform cluster are not counted. The limiter answers before the error filter, so the body is `{ statusCode, error, message }` with no `path`, `method` or `timeStamp`; the `RateLimit-*` response headers say when the window resets. |
| `500` | An unexpected error occurred. Our team has been notified. — An unhandled server-side failure. |

## GET /profile/security/login-history

**Get login history**

`operationId: SecurityController_getLoginHistory`

Recent sign-in attempts for the account, successful and failed. Failed attempts from unfamiliar locations are the signal worth acting on.

#### Signature

```http
GET /profile/security/login-history () -> Login history
```

#### Access

Requires a bearer JWT (`Authorization: Bearer <token>`).

#### Errors

Plus the standard platform errors: `401`, `403`, `429`, `500`.

#### See also

- `GET /profile/security/devices`

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `orgid` | header | string | yes | Organization (tenant) identifier. Every request is scoped to this org; data from other orgs is never visible. Issued with your API credentials. |

### Responses

| Status | Meaning |
| --- | --- |
| `200` | Login history |
| `401` | Authentication failed: Invalid or expired token — The `Authorization` header is missing, malformed, or the JWT has expired. |
| `403` | You do not have permission to perform this action — The caller is authenticated but lacks the role required by the endpoint, or is acting on another org. |
| `429` | Too Many Requests — More than 100,000 requests from one IP within 5 minutes (configurable per deployment). CORS preflights and requests from inside the platform cluster are not counted. The limiter answers before the error filter, so the body is `{ statusCode, error, message }` with no `path`, `method` or `timeStamp`; the `RateLimit-*` response headers say when the window resets. |
| `500` | An unexpected error occurred. Our team has been notified. — An unhandled server-side failure. |

## GET /profile/security/2fa/status

**Get two-factor status**

`operationId: SecurityController_getTwoFactorStatus`

Whether two-factor authentication is enabled for the account and which method is configured.

#### Signature

```http
GET /profile/security/2fa/status () -> Two-factor status
```

#### Access

Requires a bearer JWT (`Authorization: Bearer <token>`).

#### Errors

Plus the standard platform errors: `401`, `403`, `429`, `500`.

#### See also

- `POST /profile/security/2fa/setup/{method}`

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `orgid` | header | string | yes | Organization (tenant) identifier. Every request is scoped to this org; data from other orgs is never visible. Issued with your API credentials. |

### Responses

| Status | Meaning |
| --- | --- |
| `200` | Two-factor status |
| `401` | Authentication failed: Invalid or expired token — The `Authorization` header is missing, malformed, or the JWT has expired. |
| `403` | You do not have permission to perform this action — The caller is authenticated but lacks the role required by the endpoint, or is acting on another org. |
| `429` | Too Many Requests — More than 100,000 requests from one IP within 5 minutes (configurable per deployment). CORS preflights and requests from inside the platform cluster are not counted. The limiter answers before the error filter, so the body is `{ statusCode, error, message }` with no `path`, `method` or `timeStamp`; the `RateLimit-*` response headers say when the window resets. |
| `500` | An unexpected error occurred. Our team has been notified. — An unhandled server-side failure. |

## POST /profile/security/2fa/setup/{method}

**Set up two-factor authentication**

`operationId: SecurityController_setupTwoFactor`

Begins two-factor setup for a method, returning what the user needs to complete it — a QR code for an authenticator app, or triggering a code to a phone.

Setup is **not** enablement: the user must verify a code first, which proves their device actually works before the account depends on it.

#### Signature

```http
POST /profile/security/2fa/setup/{method} (method: string, body) -> Setup material — secret, QR code or confirmation a code was sent
```

#### Access

Requires a bearer JWT (`Authorization: Bearer <token>`).

#### Notes

- The setup secret is a credential. Do not log it.

#### Errors

Plus the standard platform errors: `401`, `403`, `429`, `500`.

#### See also

- `POST /profile/security/2fa/verify-setup`

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `orgid` | header | string | yes | Organization (tenant) identifier. Every request is scoped to this org; data from other orgs is never visible. Issued with your API credentials. |
| `method` | path | string | yes | Two-factor method, e.g. `totp`, `sms`, `email`. |

### Request body

Method-specific detail.

```json
{
  "phone": "+15551234567"
}
```

### Responses

| Status | Meaning |
| --- | --- |
| `201` | Setup material — secret, QR code or confirmation a code was sent |
| `401` | Authentication failed: Invalid or expired token — The `Authorization` header is missing, malformed, or the JWT has expired. |
| `403` | You do not have permission to perform this action — The caller is authenticated but lacks the role required by the endpoint, or is acting on another org. |
| `429` | Too Many Requests — More than 100,000 requests from one IP within 5 minutes (configurable per deployment). CORS preflights and requests from inside the platform cluster are not counted. The limiter answers before the error filter, so the body is `{ statusCode, error, message }` with no `path`, `method` or `timeStamp`; the `RateLimit-*` response headers say when the window resets. |
| `500` | An unexpected error occurred. Our team has been notified. — An unhandled server-side failure. |

## POST /profile/security/2fa/verify-setup

**Verify two-factor setup**

`operationId: SecurityController_verifyTwoFactorSetup`

Confirms the user can produce a valid code from their newly configured method. This is the check that stops someone locking themselves out by enabling 2FA against a device that does not work.

#### Signature

```http
POST /profile/security/2fa/verify-setup (body) -> Verification result
```

#### Access

Requires a bearer JWT (`Authorization: Bearer <token>`).

#### Errors

| Status | Code | Message | When | What to do |
| --- | --- | --- | --- | --- |
| `400` | INVALID_CODE | Invalid or expired code | The verification code is wrong or has expired. | Codes are short-lived. Request a new one rather than retrying an old code. |

Plus the standard platform errors: `401`, `403`, `429`, `500`.

#### See also

- `POST /profile/security/2fa/enable`

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `orgid` | header | string | yes | Organization (tenant) identifier. Every request is scoped to this org; data from other orgs is never visible. Issued with your API credentials. |

### Request body

The code from the authenticator or message.

```json
{
  "code": "481625"
}
```

### Responses

| Status | Meaning |
| --- | --- |
| `201` | Verification result |
| `400` | Invalid or expired code — The verification code is wrong or has expired. |
| `401` | Authentication failed: Invalid or expired token — The `Authorization` header is missing, malformed, or the JWT has expired. |
| `403` | You do not have permission to perform this action — The caller is authenticated but lacks the role required by the endpoint, or is acting on another org. |
| `429` | Too Many Requests — More than 100,000 requests from one IP within 5 minutes (configurable per deployment). CORS preflights and requests from inside the platform cluster are not counted. The limiter answers before the error filter, so the body is `{ statusCode, error, message }` with no `path`, `method` or `timeStamp`; the `RateLimit-*` response headers say when the window resets. |
| `500` | An unexpected error occurred. Our team has been notified. — An unhandled server-side failure. |

## POST /profile/security/2fa/enable

**Enable two-factor authentication**

`operationId: SecurityController_enableTwoFactor`

Turns two-factor on for the account, requiring a valid code to confirm. Generate backup codes immediately afterwards — without them, a lost device means an account recovery.

#### Signature

```http
POST /profile/security/2fa/enable (body) -> The result
```

#### Access

Requires a bearer JWT (`Authorization: Bearer <token>`).

#### Errors

| Status | Code | Message | When | What to do |
| --- | --- | --- | --- | --- |
| `400` | INVALID_CODE | Invalid or expired code | The verification code is wrong or has expired. | Codes are short-lived. Request a new one rather than retrying an old code. |

Plus the standard platform errors: `401`, `403`, `429`, `500`.

#### See also

- `POST /profile/security/2fa/backup-codes`

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `orgid` | header | string | yes | Organization (tenant) identifier. Every request is scoped to this org; data from other orgs is never visible. Issued with your API credentials. |

### Request body

A current code.

```json
{
  "code": "481625"
}
```

### Responses

| Status | Meaning |
| --- | --- |
| `201` | The result |
| `400` | Invalid or expired code — The verification code is wrong or has expired. |
| `401` | Authentication failed: Invalid or expired token — The `Authorization` header is missing, malformed, or the JWT has expired. |
| `403` | You do not have permission to perform this action — The caller is authenticated but lacks the role required by the endpoint, or is acting on another org. |
| `429` | Too Many Requests — More than 100,000 requests from one IP within 5 minutes (configurable per deployment). CORS preflights and requests from inside the platform cluster are not counted. The limiter answers before the error filter, so the body is `{ statusCode, error, message }` with no `path`, `method` or `timeStamp`; the `RateLimit-*` response headers say when the window resets. |
| `500` | An unexpected error occurred. Our team has been notified. — An unhandled server-side failure. |

## GET /profile/security/2fa/factors

**List my second factors**

`operationId: SecurityController_listFactors`

Every second factor on the caller's account — people enrol more than one (an authenticator, a phone, an email) and any verified one can answer a challenge. The default is challenged first; the list is ordered by preference. Secrets are never returned. Works for staff users and customers.

#### Signature

```http
GET /profile/security/2fa/factors () -> { twoFactorEnabled, defaultFactorId, factors }
```

#### Access

Requires a bearer JWT (`Authorization: Bearer <token>`).

#### Errors

Plus the standard platform errors: `401`, `403`, `429`, `500`.

#### See also

- `POST /profile/security/2fa/factors/{factorId}/default`
- `DELETE /profile/security/2fa/factors/{factorId}`

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `orgid` | header | string | yes | Organization (tenant) identifier. Every request is scoped to this org; data from other orgs is never visible. Issued with your API credentials. |

### Responses

| Status | Meaning |
| --- | --- |
| `200` | { twoFactorEnabled, defaultFactorId, factors } |
| `401` | Authentication failed: Invalid or expired token — The `Authorization` header is missing, malformed, or the JWT has expired. |
| `403` | You do not have permission to perform this action — The caller is authenticated but lacks the role required by the endpoint, or is acting on another org. |
| `429` | Too Many Requests — More than 100,000 requests from one IP within 5 minutes (configurable per deployment). CORS preflights and requests from inside the platform cluster are not counted. The limiter answers before the error filter, so the body is `{ statusCode, error, message }` with no `path`, `method` or `timeStamp`; the `RateLimit-*` response headers say when the window resets. |
| `500` | An unexpected error occurred. Our team has been notified. — An unhandled server-side failure. |

Example response:

```json
{
  "twoFactorEnabled": true,
  "defaultFactorId": "f1",
  "factors": [
    {
      "id": "f1",
      "type": "authenticator",
      "label": "Authenticator app",
      "status": "verified",
      "isDefault": true,
      "verifiedAt": "2026-08-02T10:00:00.000Z"
    },
    {
      "id": "f2",
      "type": "sms",
      "label": "+1 555 0100",
      "status": "verified",
      "isDefault": false
    }
  ]
}
```

## POST /profile/security/2fa/factors/{factorId}/default

**Choose which factor is challenged first**

`operationId: SecurityController_setDefaultFactor`

Makes one enrolled factor the default. Only a verified factor can be the default, or sign-in would lead with a code that has never worked.

#### Signature

```http
POST /profile/security/2fa/factors/{factorId}/default (factorId: string) -> { factorId, isDefault: true }
```

#### Access

Requires a bearer JWT (`Authorization: Bearer <token>`).

#### Errors

| Status | Code | Message | When | What to do |
| --- | --- | --- | --- | --- |
| `404` | NO_SUCH_FACTOR | No such factor on this account | `factorId` is not one of the caller's enrolled factors. | — |
| `400` | FACTOR_NOT_VERIFIED | Confirm that factor before making it the default | The factor was enrolled but never verified. | — |

Plus the standard platform errors: `401`, `403`, `429`, `500`.

#### See also

- `GET /profile/security/2fa/factors`

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `orgid` | header | string | yes | Organization (tenant) identifier. Every request is scoped to this org; data from other orgs is never visible. Issued with your API credentials. |
| `factorId` | path | string | yes | Factor id from the factor list. |

### Responses

| Status | Meaning |
| --- | --- |
| `201` | { factorId, isDefault: true } |
| `400` | Confirm that factor before making it the default — The factor was enrolled but never verified. |
| `401` | Authentication failed: Invalid or expired token — The `Authorization` header is missing, malformed, or the JWT has expired. |
| `403` | You do not have permission to perform this action — The caller is authenticated but lacks the role required by the endpoint, or is acting on another org. |
| `404` | No such factor on this account — `factorId` is not one of the caller's enrolled factors. |
| `429` | Too Many Requests — More than 100,000 requests from one IP within 5 minutes (configurable per deployment). CORS preflights and requests from inside the platform cluster are not counted. The limiter answers before the error filter, so the body is `{ statusCode, error, message }` with no `path`, `method` or `timeStamp`; the `RateLimit-*` response headers say when the window resets. |
| `500` | An unexpected error occurred. Our team has been notified. — An unhandled server-side failure. |

## DELETE /profile/security/2fa/factors/{factorId}

**Remove one second factor**

`operationId: SecurityController_removeFactor`

Removes one enrolled factor; the others stay. When the default is removed the next verified factor becomes the default. Removing the last verified factor **turns 2FA off** and deletes the backup codes, rather than leaving the account demanding a code nothing can produce.

#### Signature

```http
DELETE /profile/security/2fa/factors/{factorId} (factorId: string) -> { factorId, removed, remaining, twoFactorEnabled }
```

#### Access

Requires a bearer JWT (`Authorization: Bearer <token>`).

#### Notes

- Removing the last factor weakens the account — worth alerting on.

#### Errors

| Status | Code | Message | When | What to do |
| --- | --- | --- | --- | --- |
| `404` | NO_SUCH_FACTOR | No such factor on this account | `factorId` is not one of the caller's enrolled factors. | — |

Plus the standard platform errors: `401`, `403`, `429`, `500`.

#### See also

- `GET /profile/security/2fa/factors`

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `orgid` | header | string | yes | Organization (tenant) identifier. Every request is scoped to this org; data from other orgs is never visible. Issued with your API credentials. |
| `factorId` | path | string | yes | Factor id from the factor list. |

### Responses

| Status | Meaning |
| --- | --- |
| `200` | { factorId, removed, remaining, twoFactorEnabled } |
| `401` | Authentication failed: Invalid or expired token — The `Authorization` header is missing, malformed, or the JWT has expired. |
| `403` | You do not have permission to perform this action — The caller is authenticated but lacks the role required by the endpoint, or is acting on another org. |
| `404` | No such factor on this account — `factorId` is not one of the caller's enrolled factors. |
| `429` | Too Many Requests — More than 100,000 requests from one IP within 5 minutes (configurable per deployment). CORS preflights and requests from inside the platform cluster are not counted. The limiter answers before the error filter, so the body is `{ statusCode, error, message }` with no `path`, `method` or `timeStamp`; the `RateLimit-*` response headers say when the window resets. |
| `500` | An unexpected error occurred. Our team has been notified. — An unhandled server-side failure. |

Example response:

```json
{
  "factorId": "f2",
  "removed": true,
  "remaining": 1,
  "twoFactorEnabled": true
}
```

## POST /profile/security/2fa/disable

**Disable two-factor authentication**

`operationId: SecurityController_disableTwoFactor`

Turns two-factor off.

It requires the **password**, not a code — deliberately, since someone who has lost their second factor still needs a way out. That also makes this the endpoint an attacker with a stolen password would reach for, so it is worth alerting the user whenever it succeeds.

#### Signature

```http
POST /profile/security/2fa/disable (body) -> The result
```

#### Access

Requires a bearer JWT (`Authorization: Bearer <token>`).

#### Notes

- Notify the account owner out of band when 2FA is disabled.

#### Errors

| Status | Code | Message | When | What to do |
| --- | --- | --- | --- | --- |
| `400` | INVALID_PASSWORD | Invalid Password | The password is wrong. | Two-factor stays enabled. |

Plus the standard platform errors: `401`, `403`, `429`, `500`.

#### See also

- `POST /profile/security/2fa/enable`

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `orgid` | header | string | yes | Organization (tenant) identifier. Every request is scoped to this org; data from other orgs is never visible. Issued with your API credentials. |

### Request body

The account password.

```json
{
  "password": "…"
}
```

### Responses

| Status | Meaning |
| --- | --- |
| `201` | The result |
| `400` | Invalid Password — The password is wrong. |
| `401` | Authentication failed: Invalid or expired token — The `Authorization` header is missing, malformed, or the JWT has expired. |
| `403` | You do not have permission to perform this action — The caller is authenticated but lacks the role required by the endpoint, or is acting on another org. |
| `429` | Too Many Requests — More than 100,000 requests from one IP within 5 minutes (configurable per deployment). CORS preflights and requests from inside the platform cluster are not counted. The limiter answers before the error filter, so the body is `{ statusCode, error, message }` with no `path`, `method` or `timeStamp`; the `RateLimit-*` response headers say when the window resets. |
| `500` | An unexpected error occurred. Our team has been notified. — An unhandled server-side failure. |

## POST /profile/security/2fa/backup-codes

**Generate backup codes**

`operationId: SecurityController_generateBackupCodes`

Generates single-use recovery codes for when the second factor is unavailable.

They are shown **once** and each works once. Generating a new set invalidates the old one, so a user who regenerates without saving the new codes has thrown away their recovery route.

#### Signature

```http
POST /profile/security/2fa/backup-codes () -> The backup codes — displayed once
```

#### Access

Requires a bearer JWT (`Authorization: Bearer <token>`).

#### Notes

- Regenerating invalidates the previous set. Make the user save them before leaving the screen.

#### Errors

Plus the standard platform errors: `401`, `403`, `429`, `500`.

#### See also

- `GET /profile/security/2fa/backup-codes/count`

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `orgid` | header | string | yes | Organization (tenant) identifier. Every request is scoped to this org; data from other orgs is never visible. Issued with your API credentials. |

### Responses

| Status | Meaning |
| --- | --- |
| `201` | The backup codes — displayed once |
| `401` | Authentication failed: Invalid or expired token — The `Authorization` header is missing, malformed, or the JWT has expired. |
| `403` | You do not have permission to perform this action — The caller is authenticated but lacks the role required by the endpoint, or is acting on another org. |
| `429` | Too Many Requests — More than 100,000 requests from one IP within 5 minutes (configurable per deployment). CORS preflights and requests from inside the platform cluster are not counted. The limiter answers before the error filter, so the body is `{ statusCode, error, message }` with no `path`, `method` or `timeStamp`; the `RateLimit-*` response headers say when the window resets. |
| `500` | An unexpected error occurred. Our team has been notified. — An unhandled server-side failure. |

## GET /profile/security/2fa/backup-codes/count

**Count remaining backup codes**

`operationId: SecurityController_getBackupCodesCount`

How many unused backup codes remain — the prompt to regenerate before a user runs out entirely.

#### Signature

```http
GET /profile/security/2fa/backup-codes/count () -> Remaining code count
```

#### Access

Requires a bearer JWT (`Authorization: Bearer <token>`).

#### Errors

Plus the standard platform errors: `401`, `403`, `429`, `500`.

#### See also

- `POST /profile/security/2fa/backup-codes`

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `orgid` | header | string | yes | Organization (tenant) identifier. Every request is scoped to this org; data from other orgs is never visible. Issued with your API credentials. |

### Responses

| Status | Meaning |
| --- | --- |
| `200` | Remaining code count |
| `401` | Authentication failed: Invalid or expired token — The `Authorization` header is missing, malformed, or the JWT has expired. |
| `403` | You do not have permission to perform this action — The caller is authenticated but lacks the role required by the endpoint, or is acting on another org. |
| `429` | Too Many Requests — More than 100,000 requests from one IP within 5 minutes (configurable per deployment). CORS preflights and requests from inside the platform cluster are not counted. The limiter answers before the error filter, so the body is `{ statusCode, error, message }` with no `path`, `method` or `timeStamp`; the `RateLimit-*` response headers say when the window resets. |
| `500` | An unexpected error occurred. Our team has been notified. — An unhandled server-side failure. |

## GET /profile/security/settings

**Get security settings**

`operationId: SecurityController_getSecuritySettings`

The caller's security preferences — device trust, new-device alerts and preferred two-factor method. For a staff user it also carries `passwordSelfService`: `{ canChange, canReset, needsCurrentPassword, policy, source, rules }` from the password policy that governs them (source `user` | `group` | `role` | `default` | `none`). Account screens show or hide "Change password" from `canChange`; the change route enforces the same rule.

#### Signature

```http
GET /profile/security/settings () -> Security settings
```

#### Access

Requires a bearer JWT (`Authorization: Bearer <token>`).

#### Errors

Plus the standard platform errors: `401`, `403`, `429`, `500`.

#### See also

- `PATCH /profile/security/settings`

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `orgid` | header | string | yes | Organization (tenant) identifier. Every request is scoped to this org; data from other orgs is never visible. Issued with your API credentials. |

### Responses

| Status | Meaning |
| --- | --- |
| `200` | Security settings |
| `401` | Authentication failed: Invalid or expired token — The `Authorization` header is missing, malformed, or the JWT has expired. |
| `403` | You do not have permission to perform this action — The caller is authenticated but lacks the role required by the endpoint, or is acting on another org. |
| `429` | Too Many Requests — More than 100,000 requests from one IP within 5 minutes (configurable per deployment). CORS preflights and requests from inside the platform cluster are not counted. The limiter answers before the error filter, so the body is `{ statusCode, error, message }` with no `path`, `method` or `timeStamp`; the `RateLimit-*` response headers say when the window resets. |
| `500` | An unexpected error occurred. Our team has been notified. — An unhandled server-side failure. |

## PATCH /profile/security/settings

**Update security settings**

`operationId: SecurityController_updateSecuritySettings`

Updates the caller's security preferences.

Turning `alertOnNewDevice` off removes the notification that would tell the user about an unfamiliar sign-in — that is the setting an attacker would change first, so treat a change to it as worth logging.

#### Signature

```http
PATCH /profile/security/settings (body) -> The updated settings
```

#### Access

Requires a bearer JWT (`Authorization: Bearer <token>`).

#### Notes

- Disabling new-device alerts is a meaningful reduction in protection.

#### Errors

Plus the standard platform errors: `401`, `403`, `429`, `500`.

#### See also

- `GET /profile/security/settings`

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `orgid` | header | string | yes | Organization (tenant) identifier. Every request is scoped to this org; data from other orgs is never visible. Issued with your API credentials. |

### Request body

Settings to change.

```json
{
  "alertOnNewDevice": true,
  "preferredTwoFactorMethod": "totp"
}
```

### Responses

| Status | Meaning |
| --- | --- |
| `200` | The updated settings |
| `401` | Authentication failed: Invalid or expired token — The `Authorization` header is missing, malformed, or the JWT has expired. |
| `403` | You do not have permission to perform this action — The caller is authenticated but lacks the role required by the endpoint, or is acting on another org. |
| `429` | Too Many Requests — More than 100,000 requests from one IP within 5 minutes (configurable per deployment). CORS preflights and requests from inside the platform cluster are not counted. The limiter answers before the error filter, so the body is `{ statusCode, error, message }` with no `path`, `method` or `timeStamp`; the `RateLimit-*` response headers say when the window resets. |
| `500` | An unexpected error occurred. Our team has been notified. — An unhandled server-side failure. |

## POST /profile/security/challenge/send

**Send a security challenge**

`operationId: SecurityController_sendChallenge`

Sends a two-factor challenge code during sign-in, addressed by the challenge token issued when the password step succeeded.

Public by necessity — the caller is mid-sign-in and holds no session yet. Rate-limit it: the token is the only thing standing between an attacker and sending repeated codes to a user's phone.

#### Signature

```http
POST /profile/security/challenge/send (body) -> The result
```

#### Access

Public — no credentials required.

#### Notes

- Unauthenticated. Rate-limit per token and per account.

#### Errors

Plus the standard platform errors: `429`, `500`.

#### See also

- `POST /profile/security/challenge/verify`

### Request body

The challenge to send.

```json
{
  "challengeToken": "chl_9k2m4h1p7q",
  "method": "sms"
}
```

### Responses

| Status | Meaning |
| --- | --- |
| `201` | The result |
| `429` | Too Many Requests — More than 100,000 requests from one IP within 5 minutes (configurable per deployment). CORS preflights and requests from inside the platform cluster are not counted. The limiter answers before the error filter, so the body is `{ statusCode, error, message }` with no `path`, `method` or `timeStamp`; the `RateLimit-*` response headers say when the window resets. |
| `500` | An unexpected error occurred. Our team has been notified. — An unhandled server-side failure. |

## POST /profile/security/challenge/verify

**Verify a security challenge**

`operationId: SecurityController_verifyChallenge`

Completes two-factor sign-in by verifying the code against the challenge token.

Set `trustDevice` to skip future prompts on this device — the same caution as the trust endpoint applies, and it should never default to true on a shared machine.

#### Signature

```http
POST /profile/security/challenge/verify (body) -> Tokens on success
```

#### Access

Public — no credentials required.

#### Notes

- Limit attempts per challenge token — an unlimited retry defeats a six-digit code.

#### Errors

| Status | Code | Message | When | What to do |
| --- | --- | --- | --- | --- |
| `400` | INVALID_CODE | Invalid or expired code | The verification code is wrong or has expired. | Codes are short-lived. Request a new one rather than retrying an old code. |

Plus the standard platform errors: `429`, `500`.

#### See also

- `POST /profile/security/challenge/send`

### Request body

The challenge, the code, and whether to trust the device.

```json
{
  "challengeToken": "chl_9k2m4h1p7q",
  "code": "481625"
}
```

### Responses

| Status | Meaning |
| --- | --- |
| `201` | Tokens on success |
| `400` | Invalid or expired code — The verification code is wrong or has expired. |
| `429` | Too Many Requests — More than 100,000 requests from one IP within 5 minutes (configurable per deployment). CORS preflights and requests from inside the platform cluster are not counted. The limiter answers before the error filter, so the body is `{ statusCode, error, message }` with no `path`, `method` or `timeStamp`; the `RateLimit-*` response headers say when the window resets. |
| `500` | An unexpected error occurred. Our team has been notified. — An unhandled server-side failure. |

