# Users · Administration

Part of AppEngine API Documentation. Full index: https://appengine.appmint.io/documentation.md
## GET /profile/system-orgs

**List system organizations**

`operationId: UsersController_getSystemOrgs`

The organizations visible to the caller — what a global login offers as an org picker.

#### Signature

```http
GET /profile/system-orgs () -> Organizations
```

#### Access

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

#### Errors

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

#### See also

- `POST /global-login`

### 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` | Organizations |
| `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 /user/system-orgs

**List system organizations**

`operationId: UsersController_getSystemOrgs`

The organizations visible to the caller — what a global login offers as an org picker.

#### Signature

```http
GET /user/system-orgs () -> Organizations
```

#### Access

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

#### Errors

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

#### See also

- `POST /global-login`

### 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` | Organizations |
| `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/user/employee/candidates/{userId}

**Get employee candidates for a user**

`operationId: UsersController_employeeCandidatesForUser`

Suggests employee records that might correspond to a user account — the matching aid when linking the two.

#### Signature

```http
GET /profile/user/employee/candidates/{userId} (userId: string) -> Candidate employees
```

#### Access

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

#### Errors

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

#### See also

- `POST /user/employee/link`

### 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. |
| `userId` | path | string | yes | User id. |

### Responses

| Status | Meaning |
| --- | --- |
| `200` | Candidate employees |
| `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 /user/user/employee/candidates/{userId}

**Get employee candidates for a user**

`operationId: UsersController_employeeCandidatesForUser`

Suggests employee records that might correspond to a user account — the matching aid when linking the two.

#### Signature

```http
GET /user/user/employee/candidates/{userId} (userId: string) -> Candidate employees
```

#### Access

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

#### Errors

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

#### See also

- `POST /user/employee/link`

### 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. |
| `userId` | path | string | yes | User id. |

### Responses

| Status | Meaning |
| --- | --- |
| `200` | Candidate employees |
| `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/user/employee/{employeeId}/user-candidates

**Get user candidates for an employee**

`operationId: UsersController_userCandidatesForEmployee`

The reverse lookup: user accounts that might correspond to an employee record.

#### Signature

```http
GET /profile/user/employee/{employeeId}/user-candidates (employeeId: string) -> Candidate users
```

#### Access

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

#### Errors

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

#### See also

- `POST /user/employee/link`

### 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. |
| `employeeId` | path | string | yes | Employee id. |

### Responses

| Status | Meaning |
| --- | --- |
| `200` | Candidate users |
| `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 /user/user/employee/{employeeId}/user-candidates

**Get user candidates for an employee**

`operationId: UsersController_userCandidatesForEmployee`

The reverse lookup: user accounts that might correspond to an employee record.

#### Signature

```http
GET /user/user/employee/{employeeId}/user-candidates (employeeId: string) -> Candidate users
```

#### Access

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

#### Errors

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

#### See also

- `POST /user/employee/link`

### 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. |
| `employeeId` | path | string | yes | Employee id. |

### Responses

| Status | Meaning |
| --- | --- |
| `200` | Candidate users |
| `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/user/employee/link

**Link a user to an employee**

`operationId: UsersController_linkUserEmployee`

Connects a login account to an HR employee record, so the same person is one identity across both.

#### Signature

```http
POST /profile/user/employee/link (body) -> The link result
```

#### Access

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

#### Errors

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

#### See also

- `GET /user/employee/candidates/{userId}`

### 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 user and employee to link.

```json
{
  "userId": "USR-4821",
  "employeeId": "EMP-4821"
}
```

### Responses

| Status | Meaning |
| --- | --- |
| `201` | The link 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. |

## POST /user/user/employee/link

**Link a user to an employee**

`operationId: UsersController_linkUserEmployee`

Connects a login account to an HR employee record, so the same person is one identity across both.

#### Signature

```http
POST /user/user/employee/link (body) -> The link result
```

#### Access

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

#### Errors

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

#### See also

- `GET /user/employee/candidates/{userId}`

### 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 user and employee to link.

```json
{
  "userId": "USR-4821",
  "employeeId": "EMP-4821"
}
```

### Responses

| Status | Meaning |
| --- | --- |
| `201` | The link 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. |

## POST /profile/user/employee/create-from-user

**Create an employee from a user**

`operationId: UsersController_createEmployeeFromUser`

Creates an HR employee record from an existing login account, carrying the details across.

#### Signature

```http
POST /profile/user/employee/create-from-user (body) -> The created employee
```

#### Access

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

#### Errors

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

#### See also

- `POST /user/employee/create-from-employee`

### 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 user to base it on.

```json
{
  "userId": "USR-4821"
}
```

### Responses

| Status | Meaning |
| --- | --- |
| `201` | The created employee |
| `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 /user/user/employee/create-from-user

**Create an employee from a user**

`operationId: UsersController_createEmployeeFromUser`

Creates an HR employee record from an existing login account, carrying the details across.

#### Signature

```http
POST /user/user/employee/create-from-user (body) -> The created employee
```

#### Access

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

#### Errors

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

#### See also

- `POST /user/employee/create-from-employee`

### 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 user to base it on.

```json
{
  "userId": "USR-4821"
}
```

### Responses

| Status | Meaning |
| --- | --- |
| `201` | The created employee |
| `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/user/employee/create-from-employee

**Create a user from an employee**

`operationId: UsersController_createUserFromEmployee`

Creates a login account for an existing employee — the step that gives a new hire access once their HR record exists.

#### Signature

```http
POST /profile/user/employee/create-from-employee (body) -> The created user
```

#### Access

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

#### Errors

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

#### See also

- `POST /user/invite/send`

### 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 employee to base it on.

```json
{
  "employeeId": "EMP-4821"
}
```

### Responses

| Status | Meaning |
| --- | --- |
| `201` | The created user |
| `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 /user/user/employee/create-from-employee

**Create a user from an employee**

`operationId: UsersController_createUserFromEmployee`

Creates a login account for an existing employee — the step that gives a new hire access once their HR record exists.

#### Signature

```http
POST /user/user/employee/create-from-employee (body) -> The created user
```

#### Access

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

#### Errors

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

#### See also

- `POST /user/invite/send`

### 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 employee to base it on.

```json
{
  "employeeId": "EMP-4821"
}
```

### Responses

| Status | Meaning |
| --- | --- |
| `201` | The created user |
| `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/user/{userId}/meta

**Update another user's metadata**

`operationId: UsersController_updateUserMetaAsAdmin`

Writes metadata against another user's account. Admin-only, unlike the self-service form.

#### Signature

```http
POST /profile/user/{userId}/meta (userId: string, body) -> The updated metadata
```

#### Access

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

#### Errors

| Status | Code | Message | When | What to do |
| --- | --- | --- | --- | --- |
| `403` | ADMIN_ONLY | Only an admin can <action> | The caller lacks the admin rights the action requires. | The message names the action that was refused. |
| `400` | USER_ID_REQUIRED | User id required | The user id is missing. | Supply it in the path. |

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

#### See also

- `POST /meta`

### 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. |
| `userId` | path | string | yes | User id. |

### Request body

The metadata to write.

```json
{
  "namespace": "preferences",
  "value": {
    "theme": "dark"
  }
}
```

### Responses

| Status | Meaning |
| --- | --- |
| `201` | The updated metadata |
| `400` | User id required — The user id is missing. |
| `401` | Authentication failed: Invalid or expired token — The `Authorization` header is missing, malformed, or the JWT has expired. |
| `403` | Only an admin can <action> — The caller lacks the admin rights the action requires. |
| `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 /user/user/{userId}/meta

**Update another user's metadata**

`operationId: UsersController_updateUserMetaAsAdmin`

Writes metadata against another user's account. Admin-only, unlike the self-service form.

#### Signature

```http
POST /user/user/{userId}/meta (userId: string, body) -> The updated metadata
```

#### Access

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

#### Errors

| Status | Code | Message | When | What to do |
| --- | --- | --- | --- | --- |
| `403` | ADMIN_ONLY | Only an admin can <action> | The caller lacks the admin rights the action requires. | The message names the action that was refused. |
| `400` | USER_ID_REQUIRED | User id required | The user id is missing. | Supply it in the path. |

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

#### See also

- `POST /meta`

### 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. |
| `userId` | path | string | yes | User id. |

### Request body

The metadata to write.

```json
{
  "namespace": "preferences",
  "value": {
    "theme": "dark"
  }
}
```

### Responses

| Status | Meaning |
| --- | --- |
| `201` | The updated metadata |
| `400` | User id required — The user id is missing. |
| `401` | Authentication failed: Invalid or expired token — The `Authorization` header is missing, malformed, or the JWT has expired. |
| `403` | Only an admin can <action> — The caller lacks the admin rights the action requires. |
| `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/user/two-factor/status/{userType}/{userId}

**Read another account's second-factor state**

`operationId: UsersController_twoFactorStatusForAccount`

For an administrator looking at someone else's profile: whether 2FA is on, the method, when it was verified, how many backup codes remain, which methods the org offers, and the enrolled factors (`factors`, `defaultFactorId`, `twoFactorEnabled`). No secrets and no phone numbers beyond the factor label.

#### Signature

```http
GET /profile/user/two-factor/status/{userType}/{userId} (userType: string, userId: string) -> Second-factor state
```

#### Access

Requires a bearer JWT (`Authorization: Bearer <token>`). Required role(s): `ConfigAdmin`.

#### Errors

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

#### See also

- `POST /user/two-factor/reset`

### 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. |
| `userType` | path | "user" \| "customer" | yes | `customer` for a customer; anything else means a staff user. |
| `userId` | path | string | yes | The account `sk`. |

### Responses

| Status | Meaning |
| --- | --- |
| `200` | Second-factor state |
| `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
{
  "enabled": true,
  "method": "authenticator",
  "backupCodesRemaining": 7,
  "availableMethods": [
    "authenticator",
    "email"
  ],
  "twoFactorEnabled": true,
  "defaultFactorId": "f1",
  "factors": [
    {
      "id": "f1",
      "type": "authenticator",
      "label": "Authenticator app",
      "status": "verified",
      "isDefault": true
    }
  ]
}
```

## GET /user/user/two-factor/status/{userType}/{userId}

**Read another account's second-factor state**

`operationId: UsersController_twoFactorStatusForAccount`

For an administrator looking at someone else's profile: whether 2FA is on, the method, when it was verified, how many backup codes remain, which methods the org offers, and the enrolled factors (`factors`, `defaultFactorId`, `twoFactorEnabled`). No secrets and no phone numbers beyond the factor label.

#### Signature

```http
GET /user/user/two-factor/status/{userType}/{userId} (userType: string, userId: string) -> Second-factor state
```

#### Access

Requires a bearer JWT (`Authorization: Bearer <token>`). Required role(s): `ConfigAdmin`.

#### Errors

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

#### See also

- `POST /user/two-factor/reset`

### 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. |
| `userType` | path | "user" \| "customer" | yes | `customer` for a customer; anything else means a staff user. |
| `userId` | path | string | yes | The account `sk`. |

### Responses

| Status | Meaning |
| --- | --- |
| `200` | Second-factor state |
| `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
{
  "enabled": true,
  "method": "authenticator",
  "backupCodesRemaining": 7,
  "availableMethods": [
    "authenticator",
    "email"
  ],
  "twoFactorEnabled": true,
  "defaultFactorId": "f1",
  "factors": [
    {
      "id": "f1",
      "type": "authenticator",
      "label": "Authenticator app",
      "status": "verified",
      "isDefault": true
    }
  ]
}
```

## POST /profile/user/two-factor/reset

**Clear an account's second factor**

`operationId: UsersController_resetTwoFactor`

For an account locked out of its own 2FA — a lost authenticator with no backup codes left. Removes every enrolled factor and the backup codes and turns 2FA off; the owner enrols again from their own session. The reset is logged with the administrator and the reason.

#### Signature

```http
POST /profile/user/two-factor/reset (body) -> { userId, userType, twoFactorEnabled: false, reset: true }
```

#### Access

Requires a bearer JWT (`Authorization: Bearer <token>`). Required role(s): `ConfigAdmin`.

#### Errors

| Status | Code | Message | When | What to do |
| --- | --- | --- | --- | --- |
| `404` | NO_SECURITY_RECORD | No security record for that account | The account has never had security settings. | — |

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

#### See also

- `GET /user/two-factor/status/{userType}/{userId}`

### 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

Whose factor to clear.

```json
{
  "userId": "65f0c2a1e4b0a1b2c3d4e5f6",
  "userType": "user",
  "reason": "Lost phone, identity checked by phone call"
}
```

### Responses

| Status | Meaning |
| --- | --- |
| `201` | { userId, userType, twoFactorEnabled: false, reset: true } |
| `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 security record for that account — The account has never had security settings. |
| `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 /user/user/two-factor/reset

**Clear an account's second factor**

`operationId: UsersController_resetTwoFactor`

For an account locked out of its own 2FA — a lost authenticator with no backup codes left. Removes every enrolled factor and the backup codes and turns 2FA off; the owner enrols again from their own session. The reset is logged with the administrator and the reason.

#### Signature

```http
POST /user/user/two-factor/reset (body) -> { userId, userType, twoFactorEnabled: false, reset: true }
```

#### Access

Requires a bearer JWT (`Authorization: Bearer <token>`). Required role(s): `ConfigAdmin`.

#### Errors

| Status | Code | Message | When | What to do |
| --- | --- | --- | --- | --- |
| `404` | NO_SECURITY_RECORD | No security record for that account | The account has never had security settings. | — |

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

#### See also

- `GET /user/two-factor/status/{userType}/{userId}`

### 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

Whose factor to clear.

```json
{
  "userId": "65f0c2a1e4b0a1b2c3d4e5f6",
  "userType": "user",
  "reason": "Lost phone, identity checked by phone call"
}
```

### Responses

| Status | Meaning |
| --- | --- |
| `201` | { userId, userType, twoFactorEnabled: false, reset: true } |
| `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 security record for that account — The account has never had security settings. |
| `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/user/invite/resend/{invitationId}

**Resend an invitation**

`operationId: UsersController_inviteResend`

Re-sends an outstanding invitation — the correct response when the first was not received, since a second `send` is refused.

#### Signature

```http
POST /profile/user/invite/resend/{invitationId} (invitationId: string) -> The result
```

#### Access

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

#### Errors

| Status | Code | Message | When | What to do |
| --- | --- | --- | --- | --- |
| `404` | INVITATION_NOT_FOUND | Invitation not found | No invitation has that id. | Check the id. |

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

#### See also

- `POST /user/invite/send`

### 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. |
| `invitationId` | path | string | yes | Invitation id. |

### Responses

| Status | Meaning |
| --- | --- |
| `201` | The 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. |
| `404` | Invitation not found — No invitation has that id. |
| `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 /user/user/invite/resend/{invitationId}

**Resend an invitation**

`operationId: UsersController_inviteResend`

Re-sends an outstanding invitation — the correct response when the first was not received, since a second `send` is refused.

#### Signature

```http
POST /user/user/invite/resend/{invitationId} (invitationId: string) -> The result
```

#### Access

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

#### Errors

| Status | Code | Message | When | What to do |
| --- | --- | --- | --- | --- |
| `404` | INVITATION_NOT_FOUND | Invitation not found | No invitation has that id. | Check the id. |

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

#### See also

- `POST /user/invite/send`

### 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. |
| `invitationId` | path | string | yes | Invitation id. |

### Responses

| Status | Meaning |
| --- | --- |
| `201` | The 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. |
| `404` | Invitation not found — No invitation has that id. |
| `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/user/invite/send

**Send an invitation**

`operationId: UsersController_inviteSend`

Invites someone to join the organization. Only one active invitation per address at a time — a second is refused rather than sending a duplicate.

#### Signature

```http
POST /profile/user/invite/send (body) -> The invitation
```

#### Access

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

#### Errors

| Status | Code | Message | When | What to do |
| --- | --- | --- | --- | --- |
| `400` | INVITE_EXISTS | An active invitation already exists for this email. Please wait for it to expire. | An unexpired invitation is already outstanding. | Resend the existing invitation rather than creating another. |

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

#### See also

- `POST /user/invite/resend/{invitationId}`

### 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

Who to invite.

```json
{
  "email": "grace@example.com",
  "roles": [
    "User"
  ],
  "message": "Welcome aboard"
}
```

### Responses

| Status | Meaning |
| --- | --- |
| `201` | The invitation |
| `400` | An active invitation already exists for this email. Please wait for it to expire. — An unexpired invitation is already outstanding. |
| `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 /user/user/invite/send

**Send an invitation**

`operationId: UsersController_inviteSend`

Invites someone to join the organization. Only one active invitation per address at a time — a second is refused rather than sending a duplicate.

#### Signature

```http
POST /user/user/invite/send (body) -> The invitation
```

#### Access

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

#### Errors

| Status | Code | Message | When | What to do |
| --- | --- | --- | --- | --- |
| `400` | INVITE_EXISTS | An active invitation already exists for this email. Please wait for it to expire. | An unexpired invitation is already outstanding. | Resend the existing invitation rather than creating another. |

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

#### See also

- `POST /user/invite/resend/{invitationId}`

### 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

Who to invite.

```json
{
  "email": "grace@example.com",
  "roles": [
    "User"
  ],
  "message": "Welcome aboard"
}
```

### Responses

| Status | Meaning |
| --- | --- |
| `201` | The invitation |
| `400` | An active invitation already exists for this email. Please wait for it to expire. — An unexpired invitation is already outstanding. |
| `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/user/directory/backfill

**Rebuild the account directory**

`operationId: UsersController_directoryBackfill`

Indexes every user of every org into the account directory that sign-in without an `orgid` uses. A one-off after the directory was introduced; sign-ins keep it current afterwards. Returns how many orgs and users were indexed.

#### Signature

```http
POST /profile/user/directory/backfill () -> { orgs, users }
```

#### Access

Requires a bearer JWT (`Authorization: Bearer <token>`). Required role(s): `RootAdmin`, `ConfigAdmin`.

#### Notes

- Platform-wide: it reads every org, not just the caller's.

#### Errors

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

#### See also

- `POST /user/directory/lookup`

### 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` | { orgs, users } |
| `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
{
  "orgs": 42,
  "users": 1310
}
```

## POST /user/user/directory/backfill

**Rebuild the account directory**

`operationId: UsersController_directoryBackfill`

Indexes every user of every org into the account directory that sign-in without an `orgid` uses. A one-off after the directory was introduced; sign-ins keep it current afterwards. Returns how many orgs and users were indexed.

#### Signature

```http
POST /user/user/directory/backfill () -> { orgs, users }
```

#### Access

Requires a bearer JWT (`Authorization: Bearer <token>`). Required role(s): `RootAdmin`, `ConfigAdmin`.

#### Notes

- Platform-wide: it reads every org, not just the caller's.

#### Errors

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

#### See also

- `POST /user/directory/lookup`

### 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` | { orgs, users } |
| `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
{
  "orgs": 42,
  "users": 1310
}
```

## POST /profile/user/group/add

**Add a user to a group**

`operationId: UsersController_groupAdd`

Adds a staff user (found by `email`, `username` or `id`) to one or more groups, by group name or `sk`. Group membership carries the group's roles and permissions, so this is a privilege change — admin only. A group that grants a root role is refused outside the root org.

#### Signature

```http
POST /profile/user/group/add (body) -> The updated user
```

#### Access

Requires a bearer JWT (`Authorization: Bearer <token>`). Required role(s): `ConfigAdmin`, `RootAdmin`, `RootPowerUser`.

#### Errors

| Status | Code | Message | When | What to do |
| --- | --- | --- | --- | --- |
| `400` | GROUPS_REQUIRED | Groups are required | `groups` is empty. | — |
| `403` | ROOT_ORG_REQUIRED | Only the root organization can give a user a root role. Blocked: RootAdmin. | A root role or root group is named (or a custom role/group resolves to one) and the caller's org is not the root org. The sentence names the act and the blocked names; the body also carries `code: "ROOT_ORG_REQUIRED"` and `blocked`. | Root access is managed from the root org only. |

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

#### See also

- `POST /group/remove`

### 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 user and groups.

```json
{
  "email": "ada@example.com",
  "groups": [
    "engineering"
  ]
}
```

### Responses

| Status | Meaning |
| --- | --- |
| `201` | The updated user |
| `400` | Groups are required — `groups` is empty. |
| `401` | Authentication failed: Invalid or expired token — The `Authorization` header is missing, malformed, or the JWT has expired. |
| `403` | Only the root organization can give a user a root role. Blocked: RootAdmin. — A root role or root group is named (or a custom role/group resolves to one) and the caller's org is not the root org. The sentence names the act and the blocked names; the body also carries `code: "ROOT_ORG_REQUIRED"` and `blocked`. |
| `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/group/add

**Add a user to a group**

`operationId: UsersController_groupAdd`

Adds a staff user (found by `email`, `username` or `id`) to one or more groups, by group name or `sk`. Group membership carries the group's roles and permissions, so this is a privilege change — admin only. A group that grants a root role is refused outside the root org.

#### Signature

```http
POST /profile/group/add (body) -> The updated user
```

#### Access

Requires a bearer JWT (`Authorization: Bearer <token>`). Required role(s): `ConfigAdmin`, `RootAdmin`, `RootPowerUser`.

#### Errors

| Status | Code | Message | When | What to do |
| --- | --- | --- | --- | --- |
| `400` | GROUPS_REQUIRED | Groups are required | `groups` is empty. | — |
| `403` | ROOT_ORG_REQUIRED | Only the root organization can give a user a root role. Blocked: RootAdmin. | A root role or root group is named (or a custom role/group resolves to one) and the caller's org is not the root org. The sentence names the act and the blocked names; the body also carries `code: "ROOT_ORG_REQUIRED"` and `blocked`. | Root access is managed from the root org only. |

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

#### See also

- `POST /group/remove`

### 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 user and groups.

```json
{
  "email": "ada@example.com",
  "groups": [
    "engineering"
  ]
}
```

### Responses

| Status | Meaning |
| --- | --- |
| `201` | The updated user |
| `400` | Groups are required — `groups` is empty. |
| `401` | Authentication failed: Invalid or expired token — The `Authorization` header is missing, malformed, or the JWT has expired. |
| `403` | Only the root organization can give a user a root role. Blocked: RootAdmin. — A root role or root group is named (or a custom role/group resolves to one) and the caller's org is not the root org. The sentence names the act and the blocked names; the body also carries `code: "ROOT_ORG_REQUIRED"` and `blocked`. |
| `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 /user/user/group/add

**Add a user to a group**

`operationId: UsersController_groupAdd`

Adds a staff user (found by `email`, `username` or `id`) to one or more groups, by group name or `sk`. Group membership carries the group's roles and permissions, so this is a privilege change — admin only. A group that grants a root role is refused outside the root org.

#### Signature

```http
POST /user/user/group/add (body) -> The updated user
```

#### Access

Requires a bearer JWT (`Authorization: Bearer <token>`). Required role(s): `ConfigAdmin`, `RootAdmin`, `RootPowerUser`.

#### Errors

| Status | Code | Message | When | What to do |
| --- | --- | --- | --- | --- |
| `400` | GROUPS_REQUIRED | Groups are required | `groups` is empty. | — |
| `403` | ROOT_ORG_REQUIRED | Only the root organization can give a user a root role. Blocked: RootAdmin. | A root role or root group is named (or a custom role/group resolves to one) and the caller's org is not the root org. The sentence names the act and the blocked names; the body also carries `code: "ROOT_ORG_REQUIRED"` and `blocked`. | Root access is managed from the root org only. |

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

#### See also

- `POST /group/remove`

### 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 user and groups.

```json
{
  "email": "ada@example.com",
  "groups": [
    "engineering"
  ]
}
```

### Responses

| Status | Meaning |
| --- | --- |
| `201` | The updated user |
| `400` | Groups are required — `groups` is empty. |
| `401` | Authentication failed: Invalid or expired token — The `Authorization` header is missing, malformed, or the JWT has expired. |
| `403` | Only the root organization can give a user a root role. Blocked: RootAdmin. — A root role or root group is named (or a custom role/group resolves to one) and the caller's org is not the root org. The sentence names the act and the blocked names; the body also carries `code: "ROOT_ORG_REQUIRED"` and `blocked`. |
| `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 /user/group/add

**Add a user to a group**

`operationId: UsersController_groupAdd`

Adds a staff user (found by `email`, `username` or `id`) to one or more groups, by group name or `sk`. Group membership carries the group's roles and permissions, so this is a privilege change — admin only. A group that grants a root role is refused outside the root org.

#### Signature

```http
POST /user/group/add (body) -> The updated user
```

#### Access

Requires a bearer JWT (`Authorization: Bearer <token>`). Required role(s): `ConfigAdmin`, `RootAdmin`, `RootPowerUser`.

#### Errors

| Status | Code | Message | When | What to do |
| --- | --- | --- | --- | --- |
| `400` | GROUPS_REQUIRED | Groups are required | `groups` is empty. | — |
| `403` | ROOT_ORG_REQUIRED | Only the root organization can give a user a root role. Blocked: RootAdmin. | A root role or root group is named (or a custom role/group resolves to one) and the caller's org is not the root org. The sentence names the act and the blocked names; the body also carries `code: "ROOT_ORG_REQUIRED"` and `blocked`. | Root access is managed from the root org only. |

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

#### See also

- `POST /group/remove`

### 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 user and groups.

```json
{
  "email": "ada@example.com",
  "groups": [
    "engineering"
  ]
}
```

### Responses

| Status | Meaning |
| --- | --- |
| `201` | The updated user |
| `400` | Groups are required — `groups` is empty. |
| `401` | Authentication failed: Invalid or expired token — The `Authorization` header is missing, malformed, or the JWT has expired. |
| `403` | Only the root organization can give a user a root role. Blocked: RootAdmin. — A root role or root group is named (or a custom role/group resolves to one) and the caller's org is not the root org. The sentence names the act and the blocked names; the body also carries `code: "ROOT_ORG_REQUIRED"` and `blocked`. |
| `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/user/group/remove

**Remove a user from a group**

`operationId: UsersController_groupRemove`

Removes a staff user (by `email`) from one or more groups, withdrawing the access they conferred. Taking someone out of a root group is refused outside the root org, same as putting them in.

#### Signature

```http
POST /profile/user/group/remove (body) -> The updated user
```

#### Access

Requires a bearer JWT (`Authorization: Bearer <token>`). Required role(s): `ConfigAdmin`, `RootAdmin`, `RootPowerUser`.

#### Errors

| Status | Code | Message | When | What to do |
| --- | --- | --- | --- | --- |
| `400` | GROUPS_REQUIRED | Groups are required | `groups` is empty. | — |
| `404` | USER_NOT_FOUND | user not found | No staff user with that email. | — |
| `403` | ROOT_ORG_REQUIRED | Only the root organization can give a user a root role. Blocked: RootAdmin. | A root role or root group is named (or a custom role/group resolves to one) and the caller's org is not the root org. The sentence names the act and the blocked names; the body also carries `code: "ROOT_ORG_REQUIRED"` and `blocked`. | Root access is managed from the root org only. |

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

#### See also

- `POST /group/add`

### 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 user and groups.

```json
{
  "email": "ada@example.com",
  "groups": [
    "engineering"
  ]
}
```

### Responses

| Status | Meaning |
| --- | --- |
| `201` | The updated user |
| `400` | Groups are required — `groups` is empty. |
| `401` | Authentication failed: Invalid or expired token — The `Authorization` header is missing, malformed, or the JWT has expired. |
| `403` | Only the root organization can give a user a root role. Blocked: RootAdmin. — A root role or root group is named (or a custom role/group resolves to one) and the caller's org is not the root org. The sentence names the act and the blocked names; the body also carries `code: "ROOT_ORG_REQUIRED"` and `blocked`. |
| `404` | user not found — No staff user with that email. |
| `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/group/remove

**Remove a user from a group**

`operationId: UsersController_groupRemove`

Removes a staff user (by `email`) from one or more groups, withdrawing the access they conferred. Taking someone out of a root group is refused outside the root org, same as putting them in.

#### Signature

```http
POST /profile/group/remove (body) -> The updated user
```

#### Access

Requires a bearer JWT (`Authorization: Bearer <token>`). Required role(s): `ConfigAdmin`, `RootAdmin`, `RootPowerUser`.

#### Errors

| Status | Code | Message | When | What to do |
| --- | --- | --- | --- | --- |
| `400` | GROUPS_REQUIRED | Groups are required | `groups` is empty. | — |
| `404` | USER_NOT_FOUND | user not found | No staff user with that email. | — |
| `403` | ROOT_ORG_REQUIRED | Only the root organization can give a user a root role. Blocked: RootAdmin. | A root role or root group is named (or a custom role/group resolves to one) and the caller's org is not the root org. The sentence names the act and the blocked names; the body also carries `code: "ROOT_ORG_REQUIRED"` and `blocked`. | Root access is managed from the root org only. |

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

#### See also

- `POST /group/add`

### 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 user and groups.

```json
{
  "email": "ada@example.com",
  "groups": [
    "engineering"
  ]
}
```

### Responses

| Status | Meaning |
| --- | --- |
| `201` | The updated user |
| `400` | Groups are required — `groups` is empty. |
| `401` | Authentication failed: Invalid or expired token — The `Authorization` header is missing, malformed, or the JWT has expired. |
| `403` | Only the root organization can give a user a root role. Blocked: RootAdmin. — A root role or root group is named (or a custom role/group resolves to one) and the caller's org is not the root org. The sentence names the act and the blocked names; the body also carries `code: "ROOT_ORG_REQUIRED"` and `blocked`. |
| `404` | user not found — No staff user with that email. |
| `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 /user/user/group/remove

**Remove a user from a group**

`operationId: UsersController_groupRemove`

Removes a staff user (by `email`) from one or more groups, withdrawing the access they conferred. Taking someone out of a root group is refused outside the root org, same as putting them in.

#### Signature

```http
POST /user/user/group/remove (body) -> The updated user
```

#### Access

Requires a bearer JWT (`Authorization: Bearer <token>`). Required role(s): `ConfigAdmin`, `RootAdmin`, `RootPowerUser`.

#### Errors

| Status | Code | Message | When | What to do |
| --- | --- | --- | --- | --- |
| `400` | GROUPS_REQUIRED | Groups are required | `groups` is empty. | — |
| `404` | USER_NOT_FOUND | user not found | No staff user with that email. | — |
| `403` | ROOT_ORG_REQUIRED | Only the root organization can give a user a root role. Blocked: RootAdmin. | A root role or root group is named (or a custom role/group resolves to one) and the caller's org is not the root org. The sentence names the act and the blocked names; the body also carries `code: "ROOT_ORG_REQUIRED"` and `blocked`. | Root access is managed from the root org only. |

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

#### See also

- `POST /group/add`

### 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 user and groups.

```json
{
  "email": "ada@example.com",
  "groups": [
    "engineering"
  ]
}
```

### Responses

| Status | Meaning |
| --- | --- |
| `201` | The updated user |
| `400` | Groups are required — `groups` is empty. |
| `401` | Authentication failed: Invalid or expired token — The `Authorization` header is missing, malformed, or the JWT has expired. |
| `403` | Only the root organization can give a user a root role. Blocked: RootAdmin. — A root role or root group is named (or a custom role/group resolves to one) and the caller's org is not the root org. The sentence names the act and the blocked names; the body also carries `code: "ROOT_ORG_REQUIRED"` and `blocked`. |
| `404` | user not found — No staff user with that email. |
| `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 /user/group/remove

**Remove a user from a group**

`operationId: UsersController_groupRemove`

Removes a staff user (by `email`) from one or more groups, withdrawing the access they conferred. Taking someone out of a root group is refused outside the root org, same as putting them in.

#### Signature

```http
POST /user/group/remove (body) -> The updated user
```

#### Access

Requires a bearer JWT (`Authorization: Bearer <token>`). Required role(s): `ConfigAdmin`, `RootAdmin`, `RootPowerUser`.

#### Errors

| Status | Code | Message | When | What to do |
| --- | --- | --- | --- | --- |
| `400` | GROUPS_REQUIRED | Groups are required | `groups` is empty. | — |
| `404` | USER_NOT_FOUND | user not found | No staff user with that email. | — |
| `403` | ROOT_ORG_REQUIRED | Only the root organization can give a user a root role. Blocked: RootAdmin. | A root role or root group is named (or a custom role/group resolves to one) and the caller's org is not the root org. The sentence names the act and the blocked names; the body also carries `code: "ROOT_ORG_REQUIRED"` and `blocked`. | Root access is managed from the root org only. |

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

#### See also

- `POST /group/add`

### 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 user and groups.

```json
{
  "email": "ada@example.com",
  "groups": [
    "engineering"
  ]
}
```

### Responses

| Status | Meaning |
| --- | --- |
| `201` | The updated user |
| `400` | Groups are required — `groups` is empty. |
| `401` | Authentication failed: Invalid or expired token — The `Authorization` header is missing, malformed, or the JWT has expired. |
| `403` | Only the root organization can give a user a root role. Blocked: RootAdmin. — A root role or root group is named (or a custom role/group resolves to one) and the caller's org is not the root org. The sentence names the act and the blocked names; the body also carries `code: "ROOT_ORG_REQUIRED"` and `blocked`. |
| `404` | user not found — No staff user with that email. |
| `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/user/lockout

**Switch a user's access off or on**

`operationId: UsersController_setUserLockout`

An org owner locks (or unlocks) another staff user of the same org. A locked user is refused at sign-in and on every session check with "Account is locked. Contact your organization owner."; locking also pushes a realtime sign-out to their open sessions. Owners and system accounts cannot be locked, and nobody can lock themselves. Returns the refreshed user record.

#### Signature

```http
POST /profile/user/lockout (body) -> The user record
```

#### Access

Requires a bearer JWT (`Authorization: Bearer <token>`). Required role(s): `Owner`.

#### Errors

| Status | Code | Message | When | What to do |
| --- | --- | --- | --- | --- |
| `400` | LOCKOUT_INVALID | User ID and a boolean locked value are required | `userId` is empty or `locked` is not a boolean. | — |
| `403` | NOT_OWNER | Only an organization owner can change account access | The caller is not an Owner of this org. | — |
| `404` | USER_NOT_FOUND | User not found | No staff user with that id in this org. | — |

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

### 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

Who, and which way.

```json
{
  "userId": "65f0c2a1e4b0a1b2c3d4e5f6",
  "locked": true
}
```

### Responses

| Status | Meaning |
| --- | --- |
| `201` | The user record |
| `400` | User ID and a boolean locked value are required — `userId` is empty or `locked` is not a boolean. |
| `401` | Authentication failed: Invalid or expired token — The `Authorization` header is missing, malformed, or the JWT has expired. |
| `403` | Only an organization owner can change account access — The caller is not an Owner of this org. |
| `404` | User not found — No staff user with that id in this 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 /user/user/lockout

**Switch a user's access off or on**

`operationId: UsersController_setUserLockout`

An org owner locks (or unlocks) another staff user of the same org. A locked user is refused at sign-in and on every session check with "Account is locked. Contact your organization owner."; locking also pushes a realtime sign-out to their open sessions. Owners and system accounts cannot be locked, and nobody can lock themselves. Returns the refreshed user record.

#### Signature

```http
POST /user/user/lockout (body) -> The user record
```

#### Access

Requires a bearer JWT (`Authorization: Bearer <token>`). Required role(s): `Owner`.

#### Errors

| Status | Code | Message | When | What to do |
| --- | --- | --- | --- | --- |
| `400` | LOCKOUT_INVALID | User ID and a boolean locked value are required | `userId` is empty or `locked` is not a boolean. | — |
| `403` | NOT_OWNER | Only an organization owner can change account access | The caller is not an Owner of this org. | — |
| `404` | USER_NOT_FOUND | User not found | No staff user with that id in this org. | — |

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

### 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

Who, and which way.

```json
{
  "userId": "65f0c2a1e4b0a1b2c3d4e5f6",
  "locked": true
}
```

### Responses

| Status | Meaning |
| --- | --- |
| `201` | The user record |
| `400` | User ID and a boolean locked value are required — `userId` is empty or `locked` is not a boolean. |
| `401` | Authentication failed: Invalid or expired token — The `Authorization` header is missing, malformed, or the JWT has expired. |
| `403` | Only an organization owner can change account access — The caller is not an Owner of this org. |
| `404` | User not found — No staff user with that id in this 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/user/role/add

**Grant a role**

`operationId: UsersController_roleAdd`

Grants one or more roles to a staff user (`email` may also be the user `sk`). Built-in roles are used as named; a custom role is resolved in this org by name or `sk` and stored by its name. **This is a direct privilege escalation** — admin only, and root roles are refused outside the root org.

#### Signature

```http
POST /profile/user/role/add (body) -> The updated user
```

#### Access

Requires a bearer JWT (`Authorization: Bearer <token>`). Required role(s): `ConfigAdmin`, `RootAdmin`, `RootPowerUser`.

#### Errors

| Status | Code | Message | When | What to do |
| --- | --- | --- | --- | --- |
| `400` | ROLES_REQUIRED | User and roles are required | `email` or `roles` is empty. | — |
| `404` | ROLE_NOT_FOUND | role not found ContentReviewer | A custom role name does not exist in this org. | — |
| `403` | ROOT_ORG_REQUIRED | Only the root organization can give a user a root role. Blocked: RootAdmin. | A root role or root group is named (or a custom role/group resolves to one) and the caller's org is not the root org. The sentence names the act and the blocked names; the body also carries `code: "ROOT_ORG_REQUIRED"` and `blocked`. | Root access is managed from the root org only. |

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

#### See also

- `POST /role/remove`

### 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 user and roles.

```json
{
  "email": "ada@example.com",
  "roles": [
    "ContentAdmin"
  ]
}
```

### Responses

| Status | Meaning |
| --- | --- |
| `201` | The updated user |
| `400` | User and roles are required — `email` or `roles` is empty. |
| `401` | Authentication failed: Invalid or expired token — The `Authorization` header is missing, malformed, or the JWT has expired. |
| `403` | Only the root organization can give a user a root role. Blocked: RootAdmin. — A root role or root group is named (or a custom role/group resolves to one) and the caller's org is not the root org. The sentence names the act and the blocked names; the body also carries `code: "ROOT_ORG_REQUIRED"` and `blocked`. |
| `404` | role not found ContentReviewer — A custom role name does not exist in this 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/role/add

**Grant a role**

`operationId: UsersController_roleAdd`

Grants one or more roles to a staff user (`email` may also be the user `sk`). Built-in roles are used as named; a custom role is resolved in this org by name or `sk` and stored by its name. **This is a direct privilege escalation** — admin only, and root roles are refused outside the root org.

#### Signature

```http
POST /profile/role/add (body) -> The updated user
```

#### Access

Requires a bearer JWT (`Authorization: Bearer <token>`). Required role(s): `ConfigAdmin`, `RootAdmin`, `RootPowerUser`.

#### Errors

| Status | Code | Message | When | What to do |
| --- | --- | --- | --- | --- |
| `400` | ROLES_REQUIRED | User and roles are required | `email` or `roles` is empty. | — |
| `404` | ROLE_NOT_FOUND | role not found ContentReviewer | A custom role name does not exist in this org. | — |
| `403` | ROOT_ORG_REQUIRED | Only the root organization can give a user a root role. Blocked: RootAdmin. | A root role or root group is named (or a custom role/group resolves to one) and the caller's org is not the root org. The sentence names the act and the blocked names; the body also carries `code: "ROOT_ORG_REQUIRED"` and `blocked`. | Root access is managed from the root org only. |

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

#### See also

- `POST /role/remove`

### 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 user and roles.

```json
{
  "email": "ada@example.com",
  "roles": [
    "ContentAdmin"
  ]
}
```

### Responses

| Status | Meaning |
| --- | --- |
| `201` | The updated user |
| `400` | User and roles are required — `email` or `roles` is empty. |
| `401` | Authentication failed: Invalid or expired token — The `Authorization` header is missing, malformed, or the JWT has expired. |
| `403` | Only the root organization can give a user a root role. Blocked: RootAdmin. — A root role or root group is named (or a custom role/group resolves to one) and the caller's org is not the root org. The sentence names the act and the blocked names; the body also carries `code: "ROOT_ORG_REQUIRED"` and `blocked`. |
| `404` | role not found ContentReviewer — A custom role name does not exist in this 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 /user/user/role/add

**Grant a role**

`operationId: UsersController_roleAdd`

Grants one or more roles to a staff user (`email` may also be the user `sk`). Built-in roles are used as named; a custom role is resolved in this org by name or `sk` and stored by its name. **This is a direct privilege escalation** — admin only, and root roles are refused outside the root org.

#### Signature

```http
POST /user/user/role/add (body) -> The updated user
```

#### Access

Requires a bearer JWT (`Authorization: Bearer <token>`). Required role(s): `ConfigAdmin`, `RootAdmin`, `RootPowerUser`.

#### Errors

| Status | Code | Message | When | What to do |
| --- | --- | --- | --- | --- |
| `400` | ROLES_REQUIRED | User and roles are required | `email` or `roles` is empty. | — |
| `404` | ROLE_NOT_FOUND | role not found ContentReviewer | A custom role name does not exist in this org. | — |
| `403` | ROOT_ORG_REQUIRED | Only the root organization can give a user a root role. Blocked: RootAdmin. | A root role or root group is named (or a custom role/group resolves to one) and the caller's org is not the root org. The sentence names the act and the blocked names; the body also carries `code: "ROOT_ORG_REQUIRED"` and `blocked`. | Root access is managed from the root org only. |

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

#### See also

- `POST /role/remove`

### 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 user and roles.

```json
{
  "email": "ada@example.com",
  "roles": [
    "ContentAdmin"
  ]
}
```

### Responses

| Status | Meaning |
| --- | --- |
| `201` | The updated user |
| `400` | User and roles are required — `email` or `roles` is empty. |
| `401` | Authentication failed: Invalid or expired token — The `Authorization` header is missing, malformed, or the JWT has expired. |
| `403` | Only the root organization can give a user a root role. Blocked: RootAdmin. — A root role or root group is named (or a custom role/group resolves to one) and the caller's org is not the root org. The sentence names the act and the blocked names; the body also carries `code: "ROOT_ORG_REQUIRED"` and `blocked`. |
| `404` | role not found ContentReviewer — A custom role name does not exist in this 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 /user/role/add

**Grant a role**

`operationId: UsersController_roleAdd`

Grants one or more roles to a staff user (`email` may also be the user `sk`). Built-in roles are used as named; a custom role is resolved in this org by name or `sk` and stored by its name. **This is a direct privilege escalation** — admin only, and root roles are refused outside the root org.

#### Signature

```http
POST /user/role/add (body) -> The updated user
```

#### Access

Requires a bearer JWT (`Authorization: Bearer <token>`). Required role(s): `ConfigAdmin`, `RootAdmin`, `RootPowerUser`.

#### Errors

| Status | Code | Message | When | What to do |
| --- | --- | --- | --- | --- |
| `400` | ROLES_REQUIRED | User and roles are required | `email` or `roles` is empty. | — |
| `404` | ROLE_NOT_FOUND | role not found ContentReviewer | A custom role name does not exist in this org. | — |
| `403` | ROOT_ORG_REQUIRED | Only the root organization can give a user a root role. Blocked: RootAdmin. | A root role or root group is named (or a custom role/group resolves to one) and the caller's org is not the root org. The sentence names the act and the blocked names; the body also carries `code: "ROOT_ORG_REQUIRED"` and `blocked`. | Root access is managed from the root org only. |

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

#### See also

- `POST /role/remove`

### 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 user and roles.

```json
{
  "email": "ada@example.com",
  "roles": [
    "ContentAdmin"
  ]
}
```

### Responses

| Status | Meaning |
| --- | --- |
| `201` | The updated user |
| `400` | User and roles are required — `email` or `roles` is empty. |
| `401` | Authentication failed: Invalid or expired token — The `Authorization` header is missing, malformed, or the JWT has expired. |
| `403` | Only the root organization can give a user a root role. Blocked: RootAdmin. — A root role or root group is named (or a custom role/group resolves to one) and the caller's org is not the root org. The sentence names the act and the blocked names; the body also carries `code: "ROOT_ORG_REQUIRED"` and `blocked`. |
| `404` | role not found ContentReviewer — A custom role name does not exist in this 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/user/role/remove

**Revoke a role**

`operationId: UsersController_roleRemove`

Removes one or more roles from a staff user, withdrawing the access they granted. Takes effect on their next token — an existing access token may retain the role until it expires. Revoking a root role is refused outside the root org.

#### Signature

```http
POST /profile/user/role/remove (body) -> The updated user
```

#### Access

Requires a bearer JWT (`Authorization: Bearer <token>`). Required role(s): `ConfigAdmin`, `RootAdmin`, `RootPowerUser`.

#### Notes

- Revocation is not instant if the user holds a valid token — force a sign-out where it matters.

#### Errors

| Status | Code | Message | When | What to do |
| --- | --- | --- | --- | --- |
| `400` | ROLES_REQUIRED | Email and roles are required | `email` or `roles` is empty. | — |
| `403` | ROOT_ORG_REQUIRED | Only the root organization can give a user a root role. Blocked: RootAdmin. | A root role or root group is named (or a custom role/group resolves to one) and the caller's org is not the root org. The sentence names the act and the blocked names; the body also carries `code: "ROOT_ORG_REQUIRED"` and `blocked`. | Root access is managed from the root org only. |

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

#### See also

- `GET /logout/{userId}`

### 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 user and roles.

```json
{
  "email": "ada@example.com",
  "roles": [
    "ContentAdmin"
  ]
}
```

### Responses

| Status | Meaning |
| --- | --- |
| `201` | The updated user |
| `400` | Email and roles are required — `email` or `roles` is empty. |
| `401` | Authentication failed: Invalid or expired token — The `Authorization` header is missing, malformed, or the JWT has expired. |
| `403` | Only the root organization can give a user a root role. Blocked: RootAdmin. — A root role or root group is named (or a custom role/group resolves to one) and the caller's org is not the root org. The sentence names the act and the blocked names; the body also carries `code: "ROOT_ORG_REQUIRED"` and `blocked`. |
| `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/role/remove

**Revoke a role**

`operationId: UsersController_roleRemove`

Removes one or more roles from a staff user, withdrawing the access they granted. Takes effect on their next token — an existing access token may retain the role until it expires. Revoking a root role is refused outside the root org.

#### Signature

```http
POST /profile/role/remove (body) -> The updated user
```

#### Access

Requires a bearer JWT (`Authorization: Bearer <token>`). Required role(s): `ConfigAdmin`, `RootAdmin`, `RootPowerUser`.

#### Notes

- Revocation is not instant if the user holds a valid token — force a sign-out where it matters.

#### Errors

| Status | Code | Message | When | What to do |
| --- | --- | --- | --- | --- |
| `400` | ROLES_REQUIRED | Email and roles are required | `email` or `roles` is empty. | — |
| `403` | ROOT_ORG_REQUIRED | Only the root organization can give a user a root role. Blocked: RootAdmin. | A root role or root group is named (or a custom role/group resolves to one) and the caller's org is not the root org. The sentence names the act and the blocked names; the body also carries `code: "ROOT_ORG_REQUIRED"` and `blocked`. | Root access is managed from the root org only. |

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

#### See also

- `GET /logout/{userId}`

### 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 user and roles.

```json
{
  "email": "ada@example.com",
  "roles": [
    "ContentAdmin"
  ]
}
```

### Responses

| Status | Meaning |
| --- | --- |
| `201` | The updated user |
| `400` | Email and roles are required — `email` or `roles` is empty. |
| `401` | Authentication failed: Invalid or expired token — The `Authorization` header is missing, malformed, or the JWT has expired. |
| `403` | Only the root organization can give a user a root role. Blocked: RootAdmin. — A root role or root group is named (or a custom role/group resolves to one) and the caller's org is not the root org. The sentence names the act and the blocked names; the body also carries `code: "ROOT_ORG_REQUIRED"` and `blocked`. |
| `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 /user/user/role/remove

**Revoke a role**

`operationId: UsersController_roleRemove`

Removes one or more roles from a staff user, withdrawing the access they granted. Takes effect on their next token — an existing access token may retain the role until it expires. Revoking a root role is refused outside the root org.

#### Signature

```http
POST /user/user/role/remove (body) -> The updated user
```

#### Access

Requires a bearer JWT (`Authorization: Bearer <token>`). Required role(s): `ConfigAdmin`, `RootAdmin`, `RootPowerUser`.

#### Notes

- Revocation is not instant if the user holds a valid token — force a sign-out where it matters.

#### Errors

| Status | Code | Message | When | What to do |
| --- | --- | --- | --- | --- |
| `400` | ROLES_REQUIRED | Email and roles are required | `email` or `roles` is empty. | — |
| `403` | ROOT_ORG_REQUIRED | Only the root organization can give a user a root role. Blocked: RootAdmin. | A root role or root group is named (or a custom role/group resolves to one) and the caller's org is not the root org. The sentence names the act and the blocked names; the body also carries `code: "ROOT_ORG_REQUIRED"` and `blocked`. | Root access is managed from the root org only. |

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

#### See also

- `GET /logout/{userId}`

### 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 user and roles.

```json
{
  "email": "ada@example.com",
  "roles": [
    "ContentAdmin"
  ]
}
```

### Responses

| Status | Meaning |
| --- | --- |
| `201` | The updated user |
| `400` | Email and roles are required — `email` or `roles` is empty. |
| `401` | Authentication failed: Invalid or expired token — The `Authorization` header is missing, malformed, or the JWT has expired. |
| `403` | Only the root organization can give a user a root role. Blocked: RootAdmin. — A root role or root group is named (or a custom role/group resolves to one) and the caller's org is not the root org. The sentence names the act and the blocked names; the body also carries `code: "ROOT_ORG_REQUIRED"` and `blocked`. |
| `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 /user/role/remove

**Revoke a role**

`operationId: UsersController_roleRemove`

Removes one or more roles from a staff user, withdrawing the access they granted. Takes effect on their next token — an existing access token may retain the role until it expires. Revoking a root role is refused outside the root org.

#### Signature

```http
POST /user/role/remove (body) -> The updated user
```

#### Access

Requires a bearer JWT (`Authorization: Bearer <token>`). Required role(s): `ConfigAdmin`, `RootAdmin`, `RootPowerUser`.

#### Notes

- Revocation is not instant if the user holds a valid token — force a sign-out where it matters.

#### Errors

| Status | Code | Message | When | What to do |
| --- | --- | --- | --- | --- |
| `400` | ROLES_REQUIRED | Email and roles are required | `email` or `roles` is empty. | — |
| `403` | ROOT_ORG_REQUIRED | Only the root organization can give a user a root role. Blocked: RootAdmin. | A root role or root group is named (or a custom role/group resolves to one) and the caller's org is not the root org. The sentence names the act and the blocked names; the body also carries `code: "ROOT_ORG_REQUIRED"` and `blocked`. | Root access is managed from the root org only. |

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

#### See also

- `GET /logout/{userId}`

### 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 user and roles.

```json
{
  "email": "ada@example.com",
  "roles": [
    "ContentAdmin"
  ]
}
```

### Responses

| Status | Meaning |
| --- | --- |
| `201` | The updated user |
| `400` | Email and roles are required — `email` or `roles` is empty. |
| `401` | Authentication failed: Invalid or expired token — The `Authorization` header is missing, malformed, or the JWT has expired. |
| `403` | Only the root organization can give a user a root role. Blocked: RootAdmin. — A root role or root group is named (or a custom role/group resolves to one) and the caller's org is not the root org. The sentence names the act and the blocked names; the body also carries `code: "ROOT_ORG_REQUIRED"` and `blocked`. |
| `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/app/register

**Register an application**

`operationId: UsersController_appRegister`

Registers a client application and issues its credentials. Pass `reset` to rotate an existing app's key.

#### Signature

```http
POST /profile/app/register (reset?: string, id?: string, body) -> The registered application and its key
```

#### Access

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

#### Notes

- Rotating a key invalidates the old one — deploy the new key before rotating.

#### Errors

| Status | Code | Message | When | What to do |
| --- | --- | --- | --- | --- |
| `404` | APP_CONFIG_NOT_FOUND | app config <appId> not found for org <orgId> | The app id does not resolve for the organization. | The message names both the app and the org it looked in. |

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

#### See also

- `POST /app/key`

### 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. |
| `reset` | query | string | — | Rotate the existing key. |
| `id` | query | string | — | Existing app id to update. |

### Request body

The application to register.

```json
{
  "name": "Storefront web",
  "appId": "storefront-web"
}
```

### Responses

| Status | Meaning |
| --- | --- |
| `201` | The registered application and its key |
| `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` | app config <appId> not found for org <orgId> — The app id does not resolve for the organization. |
| `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 /user/app/register

**Register an application**

`operationId: UsersController_appRegister`

Registers a client application and issues its credentials. Pass `reset` to rotate an existing app's key.

#### Signature

```http
POST /user/app/register (reset?: string, id?: string, body) -> The registered application and its key
```

#### Access

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

#### Notes

- Rotating a key invalidates the old one — deploy the new key before rotating.

#### Errors

| Status | Code | Message | When | What to do |
| --- | --- | --- | --- | --- |
| `404` | APP_CONFIG_NOT_FOUND | app config <appId> not found for org <orgId> | The app id does not resolve for the organization. | The message names both the app and the org it looked in. |

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

#### See also

- `POST /app/key`

### 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. |
| `reset` | query | string | — | Rotate the existing key. |
| `id` | query | string | — | Existing app id to update. |

### Request body

The application to register.

```json
{
  "name": "Storefront web",
  "appId": "storefront-web"
}
```

### Responses

| Status | Meaning |
| --- | --- |
| `201` | The registered application and its key |
| `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` | app config <appId> not found for org <orgId> — The app id does not resolve for the organization. |
| `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/app/key

**Get an application key**

`operationId: UsersController_appKey`

Retrieves an application's key. The key authenticates the application, so handle the response as a secret.

#### Signature

```http
POST /profile/app/key (body) -> The application key
```

#### Access

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

#### Errors

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

#### See also

- `POST /validate-app-key`

### 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

Which application.

```json
{
  "appId": "storefront-web"
}
```

### Responses

| Status | Meaning |
| --- | --- |
| `201` | The application key |
| `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 /user/app/key

**Get an application key**

`operationId: UsersController_appKey`

Retrieves an application's key. The key authenticates the application, so handle the response as a secret.

#### Signature

```http
POST /user/app/key (body) -> The application key
```

#### Access

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

#### Errors

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

#### See also

- `POST /validate-app-key`

### 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

Which application.

```json
{
  "appId": "storefront-web"
}
```

### Responses

| Status | Meaning |
| --- | --- |
| `201` | The application key |
| `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/blacklist/get

**Get the blacklist**

`operationId: UsersController_blacklistGet`

The blocked devices, addresses and API keys. Blocked entries are refused at sign-in before credentials are checked.

#### Signature

```http
GET /profile/blacklist/get () -> Blacklist entries
```

#### Access

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

#### Errors

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

#### See also

- `POST /blacklist/add`

### 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` | Blacklist entries |
| `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 /user/blacklist/get

**Get the blacklist**

`operationId: UsersController_blacklistGet`

The blocked devices, addresses and API keys. Blocked entries are refused at sign-in before credentials are checked.

#### Signature

```http
GET /user/blacklist/get () -> Blacklist entries
```

#### Access

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

#### Errors

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

#### See also

- `POST /blacklist/add`

### 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` | Blacklist entries |
| `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/blacklist/add

**Add to the blacklist**

`operationId: UsersController_blacklistAdd`

Blocks a device or identifier from authenticating. Takes effect immediately and refuses sign-in before credentials are evaluated.

#### Signature

```http
POST /profile/blacklist/add (body) -> The blacklist entry
```

#### Access

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

#### Notes

- Blocking broadly — a shared IP, say — can lock out legitimate users.

#### Errors

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

#### See also

- `DELETE /blacklist/delete/{value}`

### 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

What to block.

```json
{
  "value": "203.0.113.42",
  "type": "ip",
  "reason": "Credential stuffing"
}
```

### Responses

| Status | Meaning |
| --- | --- |
| `201` | The blacklist entry |
| `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 /user/blacklist/add

**Add to the blacklist**

`operationId: UsersController_blacklistAdd`

Blocks a device or identifier from authenticating. Takes effect immediately and refuses sign-in before credentials are evaluated.

#### Signature

```http
POST /user/blacklist/add (body) -> The blacklist entry
```

#### Access

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

#### Notes

- Blocking broadly — a shared IP, say — can lock out legitimate users.

#### Errors

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

#### See also

- `DELETE /blacklist/delete/{value}`

### 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

What to block.

```json
{
  "value": "203.0.113.42",
  "type": "ip",
  "reason": "Credential stuffing"
}
```

### Responses

| Status | Meaning |
| --- | --- |
| `201` | The blacklist entry |
| `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/blacklist/delete/{value}

**Remove from the blacklist**

`operationId: UsersController_blacklistRemove`

Unblocks a previously blacklisted value.

#### Signature

```http
DELETE /profile/blacklist/delete/{value} (value: string) -> Removal result
```

#### Access

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

#### Errors

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

#### See also

- `POST /blacklist/add`

### 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. |
| `value` | path | string | yes | The blacklisted value. |

### 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. |

## DELETE /user/blacklist/delete/{value}

**Remove from the blacklist**

`operationId: UsersController_blacklistRemove`

Unblocks a previously blacklisted value.

#### Signature

```http
DELETE /user/blacklist/delete/{value} (value: string) -> Removal result
```

#### Access

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

#### Errors

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

#### See also

- `POST /blacklist/add`

### 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. |
| `value` | path | string | yes | The blacklisted value. |

### 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. |

## POST /profile/blacklist/apikey/add

**Blacklist an API key**

`operationId: UsersController_blacklistApiKey`

Blocks an API key — the immediate response to a leaked credential, and faster than rotating it everywhere.

#### Signature

```http
POST /profile/blacklist/apikey/add (body) -> The blacklist entry
```

#### Access

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

#### Errors

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

#### See also

- `DELETE /blacklist/apikey/delete/{apiKey}`

### 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 key to block.

```json
{
  "apiKey": "ak_9k2m4h1p7q",
  "reason": "Leaked in a public repository"
}
```

### Responses

| Status | Meaning |
| --- | --- |
| `201` | The blacklist entry |
| `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 /user/blacklist/apikey/add

**Blacklist an API key**

`operationId: UsersController_blacklistApiKey`

Blocks an API key — the immediate response to a leaked credential, and faster than rotating it everywhere.

#### Signature

```http
POST /user/blacklist/apikey/add (body) -> The blacklist entry
```

#### Access

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

#### Errors

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

#### See also

- `DELETE /blacklist/apikey/delete/{apiKey}`

### 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 key to block.

```json
{
  "apiKey": "ak_9k2m4h1p7q",
  "reason": "Leaked in a public repository"
}
```

### Responses

| Status | Meaning |
| --- | --- |
| `201` | The blacklist entry |
| `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/blacklist/apikey/delete/{apiKey}

**Unblacklist an API key**

`operationId: UsersController_unblacklistApiKey`

Restores a blocked API key.

#### Signature

```http
DELETE /profile/blacklist/apikey/delete/{apiKey} (apiKey: string) -> Removal result
```

#### Access

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

#### Errors

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

#### See also

- `POST /blacklist/apikey/add`

### 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. |
| `apiKey` | path | string | yes | The blocked key. |

### 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. |

## DELETE /user/blacklist/apikey/delete/{apiKey}

**Unblacklist an API key**

`operationId: UsersController_unblacklistApiKey`

Restores a blocked API key.

#### Signature

```http
DELETE /user/blacklist/apikey/delete/{apiKey} (apiKey: string) -> Removal result
```

#### Access

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

#### Errors

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

#### See also

- `POST /blacklist/apikey/add`

### 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. |
| `apiKey` | path | string | yes | The blocked key. |

### 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. |

