# Users

Base URL: `https://api.brixsignage.com`. Send `Authorization: Bearer $BRIX_API_KEY` unless an operation says Auth: none.

## GET /v1/users

List users

Returns the workspace roster: each person's role, which locations they can access, their sign-in method, two-factor authentication status, and last login.

**Notes.**
- Not paginated. Deleted (erased) people are not listed. A caller whose access is limited to some locations sees only the people with access there, and only those locations in `access`.

Auth: Bearer token. Permission: `user.view`.

```bash
curl "https://api.brixsignage.com/v1/users" \
  -H "Authorization: Bearer $BRIX_API_KEY"
```

Response 200: Success.

| Field | Type | Description |
| --- | --- | --- |
| `data` | array of User |  |
| `data[].id` | string |  |
| `data[].name` | string |  |
| `data[].email` | string |  |
| `data[].status` | "active" \| "invited" \| "deactivated" | `invited` until the person sets a password or signs in. |
| `data[].roleId` | string \| null | The role of the first entry in `access`; null when the person has no access. |
| `data[].scope` | string | The location name of the first entry in `access`; `—` when none. |
| `data[].loginMethod` | "sso" \| "passkey" \| "password" | How the person signs in. |
| `data[].totpEnabled` | boolean | Two-factor authentication with an authenticator app is on. |
| `data[].passkeyCount` | integer |  |
| `data[].lastLoginAt` | string \| null |  |
| `data[].access` | array of object | Each location the person can access and the role they hold there. Only locations the caller can see are listed. |
| `data[].access[].nodeId` | string | Location id. |
| `data[].access[].nodeName` | string | Location name; `—` when the location no longer exists. |
| `data[].access[].roleId` | string |  |
| `data[].access[].roleName` | string | Role name; `—` when the role no longer exists. |

Response 401: Missing, expired or revoked bearer token.

| Field | Type | Description |
| --- | --- | --- |
| `error` | string | Machine-readable code: `unauthorized`, `forbidden`, `not_found`, `validation_error`, `conflict`, `rate_limited`, `internal_error`, … |
| `message` | string | Human-readable explanation. Safe to show an operator. |
| `requestId` | string | Present on 5xx: quote it to support. |

Response 403: The token lacks the permission this operation needs (see `x-brix-permission`).

| Field | Type | Description |
| --- | --- | --- |
| `error` | string | Machine-readable code: `unauthorized`, `forbidden`, `not_found`, `validation_error`, `conflict`, `rate_limited`, `internal_error`, … |
| `message` | string | Human-readable explanation. Safe to show an operator. |
| `requestId` | string | Present on 5xx: quote it to support. |

Response 5XX: Server error. The body carries a `requestId` to quote to support.

| Field | Type | Description |
| --- | --- | --- |
| `error` | string | Machine-readable code: `unauthorized`, `forbidden`, `not_found`, `validation_error`, `conflict`, `rate_limited`, `internal_error`, … |
| `message` | string | Human-readable explanation. Safe to show an operator. |
| `requestId` | string | Present on 5xx: quote it to support. |

## PATCH /v1/users/{id}

Update user

Updates a person's profile. Supported fields are `name` and `trainingStep` (which resets a feature walkthrough for that person). Email address and account status cannot be changed through this operation. Requires access to every location the target person belongs to.

Auth: Bearer token. Permission: `user.edit`.

Parameters:

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `id` | path | string | yes | User id. |

Request body (`application/json`):

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `name` | string | no | Trimmed; must not be empty. |
| `trainingStep` | string \| null | no | Walkthrough position (`s1`…`s5`, `l1`…`l6`, `done`); null resets it. |

```bash
curl -X PATCH "https://api.brixsignage.com/v1/users/{id}" \
  -H "Authorization: Bearer $BRIX_API_KEY" \
  -H "Content-Type: application/json"
```

Response 200: Success.

| Field | Type | Description |
| --- | --- | --- |
| `data` | object |  |
| `data.id` | string |  |
| `data.email` | string |  |
| `data.name` | string |  |
| `data.status` | "active" \| "invited" \| "deactivated" |  |
| `data.trainingStep` | string \| null | Walkthrough position; null when not started or reset. |

Response 401: Missing, expired or revoked bearer token.

| Field | Type | Description |
| --- | --- | --- |
| `error` | string | Machine-readable code: `unauthorized`, `forbidden`, `not_found`, `validation_error`, `conflict`, `rate_limited`, `internal_error`, … |
| `message` | string | Human-readable explanation. Safe to show an operator. |
| `requestId` | string | Present on 5xx: quote it to support. |

Response 403: The person belongs to a location where you do not hold `user.edit`.

| Field | Type | Description |
| --- | --- | --- |
| `error` | string | Machine-readable code: `unauthorized`, `forbidden`, `not_found`, `validation_error`, `conflict`, `rate_limited`, `internal_error`, … |
| `message` | string | Human-readable explanation. Safe to show an operator. |
| `requestId` | string | Present on 5xx: quote it to support. |

Response 404: No such person in this workspace.

| Field | Type | Description |
| --- | --- | --- |
| `error` | string | Machine-readable code: `unauthorized`, `forbidden`, `not_found`, `validation_error`, `conflict`, `rate_limited`, `internal_error`, … |
| `message` | string | Human-readable explanation. Safe to show an operator. |
| `requestId` | string | Present on 5xx: quote it to support. |

Response 422: Nothing to update, an empty or over-long `name`, or an unknown `trainingStep`.

| Field | Type | Description |
| --- | --- | --- |
| `error` | string | Machine-readable code: `unauthorized`, `forbidden`, `not_found`, `validation_error`, `conflict`, `rate_limited`, `internal_error`, … |
| `message` | string | Human-readable explanation. Safe to show an operator. |
| `requestId` | string | Present on 5xx: quote it to support. |

Response 5XX: Server error. The body carries a `requestId` to quote to support.

| Field | Type | Description |
| --- | --- | --- |
| `error` | string | Machine-readable code: `unauthorized`, `forbidden`, `not_found`, `validation_error`, `conflict`, `rate_limited`, `internal_error`, … |
| `message` | string | Human-readable explanation. Safe to show an operator. |
| `requestId` | string | Present on 5xx: quote it to support. |

## POST /v1/users/{id}/deactivate

Deactivate user

Deactivates a person's account. This revokes all of their active sessions, signing them out everywhere immediately and blocking further sign-in including through SSO, and removes them from every location's approver list. This action is reversible. You cannot deactivate your own account or the last remaining account owner. Only an account owner can do this to an owner, and you cannot do it to a person who holds a permission you do not hold. An API key is never an owner.

**Notes.**
- Only an owner can deactivate an owner, and nobody can deactivate a person who holds a permission they do not hold. An API key is never an owner, whatever its permissions.

Auth: Bearer token. Permission: `user.edit`.

Parameters:

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `id` | path | string | yes | User id. |

```bash
curl -X POST "https://api.brixsignage.com/v1/users/{id}/deactivate" \
  -H "Authorization: Bearer $BRIX_API_KEY"
```

Response 200: Success.

| Field | Type | Description |
| --- | --- | --- |
| `data` | object |  |
| `data.id` | string |  |
| `data.status` | "deactivated" |  |
| `data.deactivatedAt` | string | ISO-8601 timestamp (UTC). |

Response 401: Missing, expired or revoked bearer token.

| Field | Type | Description |
| --- | --- | --- |
| `error` | string | Machine-readable code: `unauthorized`, `forbidden`, `not_found`, `validation_error`, `conflict`, `rate_limited`, `internal_error`, … |
| `message` | string | Human-readable explanation. Safe to show an operator. |
| `requestId` | string | Present on 5xx: quote it to support. |

Response 403: The person belongs to a location where you do not hold `user.edit`; `owner_required`: the person is an owner and you are not an owner there (an API key is never an owner); or `outranked`: the person holds a permission you do not hold where they hold it.

| Field | Type | Description |
| --- | --- | --- |
| `error` | string | Machine-readable code: `unauthorized`, `forbidden`, `not_found`, `validation_error`, `conflict`, `rate_limited`, `internal_error`, … |
| `message` | string | Human-readable explanation. Safe to show an operator. |
| `requestId` | string | Present on 5xx: quote it to support. |

Response 404: No such person in this workspace.

| Field | Type | Description |
| --- | --- | --- |
| `error` | string | Machine-readable code: `unauthorized`, `forbidden`, `not_found`, `validation_error`, `conflict`, `rate_limited`, `internal_error`, … |
| `message` | string | Human-readable explanation. Safe to show an operator. |
| `requestId` | string | Present on 5xx: quote it to support. |

Response 409: `cant_deactivate_self`, `already_deactivated`, or `last_owner` (the last owner of the workspace or of a location).

| Field | Type | Description |
| --- | --- | --- |
| `error` | string | Machine-readable code: `unauthorized`, `forbidden`, `not_found`, `validation_error`, `conflict`, `rate_limited`, `internal_error`, … |
| `message` | string | Human-readable explanation. Safe to show an operator. |
| `requestId` | string | Present on 5xx: quote it to support. |

Response 5XX: Server error. The body carries a `requestId` to quote to support.

| Field | Type | Description |
| --- | --- | --- |
| `error` | string | Machine-readable code: `unauthorized`, `forbidden`, `not_found`, `validation_error`, `conflict`, `rate_limited`, `internal_error`, … |
| `message` | string | Human-readable explanation. Safe to show an operator. |
| `requestId` | string | Present on 5xx: quote it to support. |

## POST /v1/users/{id}/erase

Erase user under GDPR

Permanently erases a deactivated person's personal data to satisfy a right-to-be-forgotten request: it anonymizes their profile and hard-deletes their passkeys, linked identities, sessions, and location role assignments. The account must already be deactivated. You cannot erase your own account or the last remaining account owner. Only an account owner can do this to an owner, and you cannot do it to a person who holds a permission you do not hold. An API key is never an owner.

**Notes.**
- Irreversible. The same rule as deactivation applies: only an owner can erase an owner, and the caller must hold every permission the person holds.

Auth: Bearer token. Permission: `user.edit`.

Parameters:

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `id` | path | string | yes | User id. |

```bash
curl -X POST "https://api.brixsignage.com/v1/users/{id}/erase" \
  -H "Authorization: Bearer $BRIX_API_KEY"
```

Response 200: Success.

| Field | Type | Description |
| --- | --- | --- |
| `data` | object |  |
| `data.id` | string |  |
| `data.erased` | true |  |

Response 401: Missing, expired or revoked bearer token.

| Field | Type | Description |
| --- | --- | --- |
| `error` | string | Machine-readable code: `unauthorized`, `forbidden`, `not_found`, `validation_error`, `conflict`, `rate_limited`, `internal_error`, … |
| `message` | string | Human-readable explanation. Safe to show an operator. |
| `requestId` | string | Present on 5xx: quote it to support. |

Response 403: The person belongs to a location where you do not hold `user.edit`; `owner_required`: the person is an owner and you are not an owner there (an API key is never an owner); or `outranked`: the person holds a permission you do not hold where they hold it.

| Field | Type | Description |
| --- | --- | --- |
| `error` | string | Machine-readable code: `unauthorized`, `forbidden`, `not_found`, `validation_error`, `conflict`, `rate_limited`, `internal_error`, … |
| `message` | string | Human-readable explanation. Safe to show an operator. |
| `requestId` | string | Present on 5xx: quote it to support. |

Response 404: No such person in this workspace (an erased person is not found again).

| Field | Type | Description |
| --- | --- | --- |
| `error` | string | Machine-readable code: `unauthorized`, `forbidden`, `not_found`, `validation_error`, `conflict`, `rate_limited`, `internal_error`, … |
| `message` | string | Human-readable explanation. Safe to show an operator. |
| `requestId` | string | Present on 5xx: quote it to support. |

Response 409: `cant_erase_self`, `last_owner`, or `must_deactivate_first`.

| Field | Type | Description |
| --- | --- | --- |
| `error` | string | Machine-readable code: `unauthorized`, `forbidden`, `not_found`, `validation_error`, `conflict`, `rate_limited`, `internal_error`, … |
| `message` | string | Human-readable explanation. Safe to show an operator. |
| `requestId` | string | Present on 5xx: quote it to support. |

Response 5XX: Server error. The body carries a `requestId` to quote to support.

| Field | Type | Description |
| --- | --- | --- |
| `error` | string | Machine-readable code: `unauthorized`, `forbidden`, `not_found`, `validation_error`, `conflict`, `rate_limited`, `internal_error`, … |
| `message` | string | Human-readable explanation. Safe to show an operator. |
| `requestId` | string | Present on 5xx: quote it to support. |

## POST /v1/users/{id}/reactivate

Reactivate user

Restores a deactivated person's account to active status. Their previous role assignments are restored along with the access they grant. Any approver-list entries removed at deactivation are not automatically restored. Only an account owner can do this to an owner, and you cannot do it to a person who holds a permission you do not hold. An API key is never an owner.

Auth: Bearer token. Permission: `user.edit`.

Parameters:

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `id` | path | string | yes | User id. |

```bash
curl -X POST "https://api.brixsignage.com/v1/users/{id}/reactivate" \
  -H "Authorization: Bearer $BRIX_API_KEY"
```

Response 200: Success.

| Field | Type | Description |
| --- | --- | --- |
| `data` | object |  |
| `data.id` | string |  |
| `data.status` | "active" |  |

Response 401: Missing, expired or revoked bearer token.

| Field | Type | Description |
| --- | --- | --- |
| `error` | string | Machine-readable code: `unauthorized`, `forbidden`, `not_found`, `validation_error`, `conflict`, `rate_limited`, `internal_error`, … |
| `message` | string | Human-readable explanation. Safe to show an operator. |
| `requestId` | string | Present on 5xx: quote it to support. |

Response 403: The person belongs to a location where you do not hold `user.edit`; `owner_required`: the person is an owner and you are not an owner there (an API key is never an owner); or `outranked`: the person holds a permission you do not hold where they hold it.

| Field | Type | Description |
| --- | --- | --- |
| `error` | string | Machine-readable code: `unauthorized`, `forbidden`, `not_found`, `validation_error`, `conflict`, `rate_limited`, `internal_error`, … |
| `message` | string | Human-readable explanation. Safe to show an operator. |
| `requestId` | string | Present on 5xx: quote it to support. |

Response 404: No such person in this workspace.

| Field | Type | Description |
| --- | --- | --- |
| `error` | string | Machine-readable code: `unauthorized`, `forbidden`, `not_found`, `validation_error`, `conflict`, `rate_limited`, `internal_error`, … |
| `message` | string | Human-readable explanation. Safe to show an operator. |
| `requestId` | string | Present on 5xx: quote it to support. |

Response 409: `not_deactivated`: the person is not deactivated.

| Field | Type | Description |
| --- | --- | --- |
| `error` | string | Machine-readable code: `unauthorized`, `forbidden`, `not_found`, `validation_error`, `conflict`, `rate_limited`, `internal_error`, … |
| `message` | string | Human-readable explanation. Safe to show an operator. |
| `requestId` | string | Present on 5xx: quote it to support. |

Response 5XX: Server error. The body carries a `requestId` to quote to support.

| Field | Type | Description |
| --- | --- | --- |
| `error` | string | Machine-readable code: `unauthorized`, `forbidden`, `not_found`, `validation_error`, `conflict`, `rate_limited`, `internal_error`, … |
| `message` | string | Human-readable explanation. Safe to show an operator. |
| `requestId` | string | Present on 5xx: quote it to support. |

## GET /v1/users/{id}/sessions

List user sessions

Returns a person's currently active sign-in sessions: id, creation time, last used time, expiry, device or browser, IP address, and which session belongs to the caller. Revoked and expired sessions are not included.

**Notes.**
- Needs `user.edit`, not `user.view`: the rows carry IP addresses. Sorted by last use, newest first.

Auth: Bearer token. Permission: `user.edit`.

Parameters:

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `id` | path | string | yes | User id. |

```bash
curl "https://api.brixsignage.com/v1/users/{id}/sessions" \
  -H "Authorization: Bearer $BRIX_API_KEY"
```

Response 200: Success.

| Field | Type | Description |
| --- | --- | --- |
| `data` | array of UserSession |  |
| `data[].id` | string | Session id (not the session token, which is never returned). |
| `data[].createdAt` | string | ISO-8601 timestamp (UTC). |
| `data[].lastUsedAt` | string \| null |  |
| `data[].expiresAt` | string | ISO-8601 timestamp (UTC). |
| `data[].userAgent` | string \| null | The browser or device that signed in. |
| `data[].ipAddress` | string \| null |  |
| `data[].current` | boolean | This is the session making the call (always false for an API key). |

Response 401: Missing, expired or revoked bearer token.

| Field | Type | Description |
| --- | --- | --- |
| `error` | string | Machine-readable code: `unauthorized`, `forbidden`, `not_found`, `validation_error`, `conflict`, `rate_limited`, `internal_error`, … |
| `message` | string | Human-readable explanation. Safe to show an operator. |
| `requestId` | string | Present on 5xx: quote it to support. |

Response 403: The person belongs to a location where you do not hold `user.edit`.

| Field | Type | Description |
| --- | --- | --- |
| `error` | string | Machine-readable code: `unauthorized`, `forbidden`, `not_found`, `validation_error`, `conflict`, `rate_limited`, `internal_error`, … |
| `message` | string | Human-readable explanation. Safe to show an operator. |
| `requestId` | string | Present on 5xx: quote it to support. |

Response 404: No such person in this workspace.

| Field | Type | Description |
| --- | --- | --- |
| `error` | string | Machine-readable code: `unauthorized`, `forbidden`, `not_found`, `validation_error`, `conflict`, `rate_limited`, `internal_error`, … |
| `message` | string | Human-readable explanation. Safe to show an operator. |
| `requestId` | string | Present on 5xx: quote it to support. |

Response 5XX: Server error. The body carries a `requestId` to quote to support.

| Field | Type | Description |
| --- | --- | --- |
| `error` | string | Machine-readable code: `unauthorized`, `forbidden`, `not_found`, `validation_error`, `conflict`, `rate_limited`, `internal_error`, … |
| `message` | string | Human-readable explanation. Safe to show an operator. |
| `requestId` | string | Present on 5xx: quote it to support. |

## DELETE /v1/users/{id}/sessions/{sessionId}

Revoke user session

Signs one of a person's devices out by revoking that session. The session record itself is kept, not deleted. Calling this on an already-revoked session is safe and reports `revoked: false`.

Auth: Bearer token. Permission: `user.edit`.

Parameters:

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `id` | path | string | yes | User id. |
| `sessionId` | path | string | yes | Session id from the session list. |

```bash
curl -X DELETE "https://api.brixsignage.com/v1/users/{id}/sessions/{sessionId}" \
  -H "Authorization: Bearer $BRIX_API_KEY"
```

Response 200: Success.

| Field | Type | Description |
| --- | --- | --- |
| `data` | object |  |
| `data.id` | string |  |
| `data.revoked` | boolean | False when the session was already revoked. |

Response 401: Missing, expired or revoked bearer token.

| Field | Type | Description |
| --- | --- | --- |
| `error` | string | Machine-readable code: `unauthorized`, `forbidden`, `not_found`, `validation_error`, `conflict`, `rate_limited`, `internal_error`, … |
| `message` | string | Human-readable explanation. Safe to show an operator. |
| `requestId` | string | Present on 5xx: quote it to support. |

Response 403: The person belongs to a location where you do not hold `user.edit`.

| Field | Type | Description |
| --- | --- | --- |
| `error` | string | Machine-readable code: `unauthorized`, `forbidden`, `not_found`, `validation_error`, `conflict`, `rate_limited`, `internal_error`, … |
| `message` | string | Human-readable explanation. Safe to show an operator. |
| `requestId` | string | Present on 5xx: quote it to support. |

Response 404: No such person, or no such session for this person.

| Field | Type | Description |
| --- | --- | --- |
| `error` | string | Machine-readable code: `unauthorized`, `forbidden`, `not_found`, `validation_error`, `conflict`, `rate_limited`, `internal_error`, … |
| `message` | string | Human-readable explanation. Safe to show an operator. |
| `requestId` | string | Present on 5xx: quote it to support. |

Response 5XX: Server error. The body carries a `requestId` to quote to support.

| Field | Type | Description |
| --- | --- | --- |
| `error` | string | Machine-readable code: `unauthorized`, `forbidden`, `not_found`, `validation_error`, `conflict`, `rate_limited`, `internal_error`, … |
| `message` | string | Human-readable explanation. Safe to show an operator. |
| `requestId` | string | Present on 5xx: quote it to support. |

## POST /v1/users/{id}/sessions/revoke-all

Revoke all user sessions

Signs a person out of every active session without deactivating their account, so they can still sign back in afterward. Returns the number of sessions that were revoked.

Auth: Bearer token. Permission: `user.edit`.

Parameters:

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `id` | path | string | yes | User id. |

```bash
curl -X POST "https://api.brixsignage.com/v1/users/{id}/sessions/revoke-all" \
  -H "Authorization: Bearer $BRIX_API_KEY"
```

Response 200: Success.

| Field | Type | Description |
| --- | --- | --- |
| `data` | object |  |
| `data.id` | string | User id. |
| `data.revoked` | integer | Sessions revoked. |

Response 401: Missing, expired or revoked bearer token.

| Field | Type | Description |
| --- | --- | --- |
| `error` | string | Machine-readable code: `unauthorized`, `forbidden`, `not_found`, `validation_error`, `conflict`, `rate_limited`, `internal_error`, … |
| `message` | string | Human-readable explanation. Safe to show an operator. |
| `requestId` | string | Present on 5xx: quote it to support. |

Response 403: The person belongs to a location where you do not hold `user.edit`.

| Field | Type | Description |
| --- | --- | --- |
| `error` | string | Machine-readable code: `unauthorized`, `forbidden`, `not_found`, `validation_error`, `conflict`, `rate_limited`, `internal_error`, … |
| `message` | string | Human-readable explanation. Safe to show an operator. |
| `requestId` | string | Present on 5xx: quote it to support. |

Response 404: No such person in this workspace.

| Field | Type | Description |
| --- | --- | --- |
| `error` | string | Machine-readable code: `unauthorized`, `forbidden`, `not_found`, `validation_error`, `conflict`, `rate_limited`, `internal_error`, … |
| `message` | string | Human-readable explanation. Safe to show an operator. |
| `requestId` | string | Present on 5xx: quote it to support. |

Response 5XX: Server error. The body carries a `requestId` to quote to support.

| Field | Type | Description |
| --- | --- | --- |
| `error` | string | Machine-readable code: `unauthorized`, `forbidden`, `not_found`, `validation_error`, `conflict`, `rate_limited`, `internal_error`, … |
| `message` | string | Human-readable explanation. Safe to show an operator. |
| `requestId` | string | Present on 5xx: quote it to support. |

## POST /v1/users/invite

Invite user

Creates a new person in the workspace, or reuses an existing one, and assigns them a role at the chosen locations. A new person gets an email with a link to set their password. When a signed-in user sends the invite, the email goes out only when that user's own email address is verified. An invite made with an API key sends the email in the workspace's name. You cannot grant a role carrying permissions you do not hold yourself.

**Notes.**
- Answers 200 (not 201) for a new person too; `created` tells the two apart.
- For a signed-in user, the set-password email is sent only when that user's own email address is verified. An invite made with an API key sends the email too, from the workspace (the key's name is not shown). The 30-a-minute limit counts per key for a key.

Auth: Bearer token. Permission: `user.edit`.

Request body (`application/json`):

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `email` | string | yes | Stored in lower case. |
| `name` | string | no | Used only when the person is new. |
| `roleId` | string | yes |  |
| `nodeIds` | array of string | yes | Locations to grant the role at. You need `user.edit` and every permission of the role at each one. |

```bash
curl -X POST "https://api.brixsignage.com/v1/users/invite" \
  -H "Authorization: Bearer $BRIX_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"email":"sam@example.com","name":"Sam Rivera","roleId":"role_5e6f7a8b9c0d1e2f","nodeIds":["on_4d5e6f7a8b9c0d1e"]}'
```

Response 200: Success.

| Field | Type | Description |
| --- | --- | --- |
| `data` | object |  |
| `data.id` | string |  |
| `data.email` | string |  |
| `data.status` | "active" \| "invited" \| "deactivated" |  |
| `data.created` | boolean | True when a new person was created; false when an existing person got the extra access. |
| `data.emailSent` | boolean | A set-password email went out. |
| `data.emailBlockedReason` | "inviter-unverified" \| "provider-refused" \| null | Why no email went out for a new person: the calling user's own email address is not verified (never for an API key), or the mail provider refused it. Null when sent, or when the person already existed. |

Response 401: Missing, expired or revoked bearer token.

| Field | Type | Description |
| --- | --- | --- |
| `error` | string | Machine-readable code: `unauthorized`, `forbidden`, `not_found`, `validation_error`, `conflict`, `rate_limited`, `internal_error`, … |
| `message` | string | Human-readable explanation. Safe to show an operator. |
| `requestId` | string | Present on 5xx: quote it to support. |

Response 403: The role holds permissions you do not hold, or you lack `user.edit` or the role's permissions at one of the locations.

| Field | Type | Description |
| --- | --- | --- |
| `error` | string | Machine-readable code: `unauthorized`, `forbidden`, `not_found`, `validation_error`, `conflict`, `rate_limited`, `internal_error`, … |
| `message` | string | Human-readable explanation. Safe to show an operator. |
| `requestId` | string | Present on 5xx: quote it to support. |

Response 404: No such role or location in this workspace.

| Field | Type | Description |
| --- | --- | --- |
| `error` | string | Machine-readable code: `unauthorized`, `forbidden`, `not_found`, `validation_error`, `conflict`, `rate_limited`, `internal_error`, … |
| `message` | string | Human-readable explanation. Safe to show an operator. |
| `requestId` | string | Present on 5xx: quote it to support. |

Response 422: Invalid `email`, or `roleId` / `nodeIds` missing.

| Field | Type | Description |
| --- | --- | --- |
| `error` | string | Machine-readable code: `unauthorized`, `forbidden`, `not_found`, `validation_error`, `conflict`, `rate_limited`, `internal_error`, … |
| `message` | string | Human-readable explanation. Safe to show an operator. |
| `requestId` | string | Present on 5xx: quote it to support. |

Response 429: More than 30 invites a minute.

| Field | Type | Description |
| --- | --- | --- |
| `error` | string | Machine-readable code: `unauthorized`, `forbidden`, `not_found`, `validation_error`, `conflict`, `rate_limited`, `internal_error`, … |
| `message` | string | Human-readable explanation. Safe to show an operator. |
| `requestId` | string | Present on 5xx: quote it to support. |

Response 5XX: Server error. The body carries a `requestId` to quote to support.

| Field | Type | Description |
| --- | --- | --- |
| `error` | string | Machine-readable code: `unauthorized`, `forbidden`, `not_found`, `validation_error`, `conflict`, `rate_limited`, `internal_error`, … |
| `message` | string | Human-readable explanation. Safe to show an operator. |
| `requestId` | string | Present on 5xx: quote it to support. |
