# Users · Authentication

Part of AppEngine API Documentation. Full index: https://appengine.appmint.io/documentation.md
## POST /profile/signout

**Sign out**

`operationId: UsersController_signOut`

Ends the current session and invalidates its tokens.

#### Signature

```http
POST /profile/signout (body) -> Sign-out result
```

#### Access

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

#### Errors

Plus the standard platform errors: `401`, `403`, `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

Optional session detail.

```json
{}
```

### Responses

| Status | Meaning |
| --- | --- |
| `201` | Sign-out 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/signout

**Sign out**

`operationId: UsersController_signOut`

Ends the current session and invalidates its tokens.

#### Signature

```http
POST /user/signout (body) -> Sign-out result
```

#### Access

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

#### Errors

Plus the standard platform errors: `401`, `403`, `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

Optional session detail.

```json
{}
```

### Responses

| Status | Meaning |
| --- | --- |
| `201` | Sign-out 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/guest/auth

**Authenticate as a guest**

`operationId: UsersController_guestAuth`

Issues a limited token for an unauthenticated visitor, so a storefront can act on their behalf before they have an account.

#### Signature

```http
POST /profile/guest/auth (body) -> A guest token
```

#### Access

Public — no credentials required.

#### Notes

- Guest tokens are still credentials — scope them narrowly.

#### Errors

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

#### See also

- `POST /app/register`

### 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. |
| `appKey` | header | string | yes | Application Key |
| `appId` | header | string | yes | Application requesting guest access. |

### Request body

Guest context.

```json
{}
```

### Responses

| Status | Meaning |
| --- | --- |
| `201` | A guest token |
| `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/guest/auth

**Authenticate as a guest**

`operationId: UsersController_guestAuth`

Issues a limited token for an unauthenticated visitor, so a storefront can act on their behalf before they have an account.

#### Signature

```http
POST /user/guest/auth (body) -> A guest token
```

#### Access

Public — no credentials required.

#### Notes

- Guest tokens are still credentials — scope them narrowly.

#### Errors

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

#### See also

- `POST /app/register`

### 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. |
| `appKey` | header | string | yes | Application Key |
| `appId` | header | string | yes | Application requesting guest access. |

### Request body

Guest context.

```json
{}
```

### Responses

| Status | Meaning |
| --- | --- |
| `201` | A guest token |
| `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/signin

**Sign in**

`operationId: UsersController_signin`

Authenticates a user and issues tokens.

**Without an `orgid` header** the email finds the org through the account directory: when the password matches in exactly one org the session is issued for it (with `orgName`); when it matches in several the answer is `{ requiresOrgChoice: true, orgs }` and the client signs in again with the chosen `orgid`.

The answer may be a step rather than a session: a second-factor challenge (`requiresTwoFactor`, `challengeToken`, `twoFactorMethod` — the code is already sent unless the method is `authenticator`), a new-device check (`isNewDevice: true`, when the org turns on new-device verification), or tokens with `requiresPasswordChange: true` after a temporary password. Otherwise it is `{ user, orgId, rootOrg, sharedOrg, token, refreshToken }`.

A wrong password and an unknown account produce the **same** error, deliberately — distinguishing them would let an attacker enumerate accounts. Do not add a more helpful message on the client.

Repeated failures can lock the account, and a blocked device is refused before credentials are even checked.

#### Signature

```http
POST /profile/user/signin (body) -> A session, or the next step to take
```

#### Access

Public — no credentials required.

#### Notes

- Rate-limit this endpoint — the error deliberately gives an attacker nothing, but attempts are still cheap.

#### Errors

| Status | Code | Message | When | What to do |
| --- | --- | --- | --- | --- |
| `400` | INVALID_CREDENTIALS | Invalid username or password | The identifier or password is wrong. | Deliberately does not distinguish an unknown account from a wrong password — do not surface a more specific message to the caller. |
| `403` | ACCOUNT_LOCKED | Account is locked. Please contact support. | The account has been locked, typically after repeated failed attempts. | An operator must unlock it. Retrying does not help. |

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

#### See also

- `POST /user/refresh`
- `POST /signin/passcode`

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `orgid` | header | string | — | The org to sign into. Omit it to let the email find the org (see the description). |
| `x-client-info` | header | string | — | Client context recorded against the attempt — used for device tracking and blocking. |

### Request body

Credentials.

```json
{
  "email": "ada@example.com",
  "password": "correct horse battery staple"
}
```

### Responses

| Status | Meaning |
| --- | --- |
| `201` | A session, or the next step to take |
| `400` | Invalid username or password — The identifier or password is wrong. |
| `403` | Account is locked. Please contact support. — The account has been locked, typically after repeated failed attempts. |
| `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
{
  "token": "eyJhbGciOi…",
  "refreshToken": "eyJhbGciOi…",
  "orgId": "acme",
  "user": {
    "sk": "65f0c2a1e4b0a1b2c3d4e5f6",
    "data": {
      "email": "ada@example.com"
    }
  }
}
```

## POST /profile/signin

**Sign in**

`operationId: UsersController_signin`

Authenticates a user and issues tokens.

**Without an `orgid` header** the email finds the org through the account directory: when the password matches in exactly one org the session is issued for it (with `orgName`); when it matches in several the answer is `{ requiresOrgChoice: true, orgs }` and the client signs in again with the chosen `orgid`.

The answer may be a step rather than a session: a second-factor challenge (`requiresTwoFactor`, `challengeToken`, `twoFactorMethod` — the code is already sent unless the method is `authenticator`), a new-device check (`isNewDevice: true`, when the org turns on new-device verification), or tokens with `requiresPasswordChange: true` after a temporary password. Otherwise it is `{ user, orgId, rootOrg, sharedOrg, token, refreshToken }`.

A wrong password and an unknown account produce the **same** error, deliberately — distinguishing them would let an attacker enumerate accounts. Do not add a more helpful message on the client.

Repeated failures can lock the account, and a blocked device is refused before credentials are even checked.

#### Signature

```http
POST /profile/signin (body) -> A session, or the next step to take
```

#### Access

Public — no credentials required.

#### Notes

- Rate-limit this endpoint — the error deliberately gives an attacker nothing, but attempts are still cheap.

#### Errors

| Status | Code | Message | When | What to do |
| --- | --- | --- | --- | --- |
| `400` | INVALID_CREDENTIALS | Invalid username or password | The identifier or password is wrong. | Deliberately does not distinguish an unknown account from a wrong password — do not surface a more specific message to the caller. |
| `403` | ACCOUNT_LOCKED | Account is locked. Please contact support. | The account has been locked, typically after repeated failed attempts. | An operator must unlock it. Retrying does not help. |

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

#### See also

- `POST /user/refresh`
- `POST /signin/passcode`

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `orgid` | header | string | — | The org to sign into. Omit it to let the email find the org (see the description). |
| `x-client-info` | header | string | — | Client context recorded against the attempt — used for device tracking and blocking. |

### Request body

Credentials.

```json
{
  "email": "ada@example.com",
  "password": "correct horse battery staple"
}
```

### Responses

| Status | Meaning |
| --- | --- |
| `201` | A session, or the next step to take |
| `400` | Invalid username or password — The identifier or password is wrong. |
| `403` | Account is locked. Please contact support. — The account has been locked, typically after repeated failed attempts. |
| `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
{
  "token": "eyJhbGciOi…",
  "refreshToken": "eyJhbGciOi…",
  "orgId": "acme",
  "user": {
    "sk": "65f0c2a1e4b0a1b2c3d4e5f6",
    "data": {
      "email": "ada@example.com"
    }
  }
}
```

## POST /user/user/signin

**Sign in**

`operationId: UsersController_signin`

Authenticates a user and issues tokens.

**Without an `orgid` header** the email finds the org through the account directory: when the password matches in exactly one org the session is issued for it (with `orgName`); when it matches in several the answer is `{ requiresOrgChoice: true, orgs }` and the client signs in again with the chosen `orgid`.

The answer may be a step rather than a session: a second-factor challenge (`requiresTwoFactor`, `challengeToken`, `twoFactorMethod` — the code is already sent unless the method is `authenticator`), a new-device check (`isNewDevice: true`, when the org turns on new-device verification), or tokens with `requiresPasswordChange: true` after a temporary password. Otherwise it is `{ user, orgId, rootOrg, sharedOrg, token, refreshToken }`.

A wrong password and an unknown account produce the **same** error, deliberately — distinguishing them would let an attacker enumerate accounts. Do not add a more helpful message on the client.

Repeated failures can lock the account, and a blocked device is refused before credentials are even checked.

#### Signature

```http
POST /user/user/signin (body) -> A session, or the next step to take
```

#### Access

Public — no credentials required.

#### Notes

- Rate-limit this endpoint — the error deliberately gives an attacker nothing, but attempts are still cheap.

#### Errors

| Status | Code | Message | When | What to do |
| --- | --- | --- | --- | --- |
| `400` | INVALID_CREDENTIALS | Invalid username or password | The identifier or password is wrong. | Deliberately does not distinguish an unknown account from a wrong password — do not surface a more specific message to the caller. |
| `403` | ACCOUNT_LOCKED | Account is locked. Please contact support. | The account has been locked, typically after repeated failed attempts. | An operator must unlock it. Retrying does not help. |

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

#### See also

- `POST /user/refresh`
- `POST /signin/passcode`

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `orgid` | header | string | — | The org to sign into. Omit it to let the email find the org (see the description). |
| `x-client-info` | header | string | — | Client context recorded against the attempt — used for device tracking and blocking. |

### Request body

Credentials.

```json
{
  "email": "ada@example.com",
  "password": "correct horse battery staple"
}
```

### Responses

| Status | Meaning |
| --- | --- |
| `201` | A session, or the next step to take |
| `400` | Invalid username or password — The identifier or password is wrong. |
| `403` | Account is locked. Please contact support. — The account has been locked, typically after repeated failed attempts. |
| `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
{
  "token": "eyJhbGciOi…",
  "refreshToken": "eyJhbGciOi…",
  "orgId": "acme",
  "user": {
    "sk": "65f0c2a1e4b0a1b2c3d4e5f6",
    "data": {
      "email": "ada@example.com"
    }
  }
}
```

## POST /user/signin

**Sign in**

`operationId: UsersController_signin`

Authenticates a user and issues tokens.

**Without an `orgid` header** the email finds the org through the account directory: when the password matches in exactly one org the session is issued for it (with `orgName`); when it matches in several the answer is `{ requiresOrgChoice: true, orgs }` and the client signs in again with the chosen `orgid`.

The answer may be a step rather than a session: a second-factor challenge (`requiresTwoFactor`, `challengeToken`, `twoFactorMethod` — the code is already sent unless the method is `authenticator`), a new-device check (`isNewDevice: true`, when the org turns on new-device verification), or tokens with `requiresPasswordChange: true` after a temporary password. Otherwise it is `{ user, orgId, rootOrg, sharedOrg, token, refreshToken }`.

A wrong password and an unknown account produce the **same** error, deliberately — distinguishing them would let an attacker enumerate accounts. Do not add a more helpful message on the client.

Repeated failures can lock the account, and a blocked device is refused before credentials are even checked.

#### Signature

```http
POST /user/signin (body) -> A session, or the next step to take
```

#### Access

Public — no credentials required.

#### Notes

- Rate-limit this endpoint — the error deliberately gives an attacker nothing, but attempts are still cheap.
- Access gate: when an active requirement rule has `enforcement.access` block/override with `gatedRoles`/`gatedGroups` this person holds, and the requirement is unmet with no active override, those roles (and every role those groups grant) are withheld from the resolved grants. `user.data.readinessWithheld = { roles[], groups[], reasons[] }` says why. Root* grants are never withheld; root-org protection is unchanged.
- Workforce Readiness: with no requirement rules this endpoint behaves exactly as before. `warn` never stops anything — it comes back as `readiness.warnings` in the response.

#### Errors

| Status | Code | Message | When | What to do |
| --- | --- | --- | --- | --- |
| `400` | INVALID_CREDENTIALS | Invalid username or password | The identifier or password is wrong. | Deliberately does not distinguish an unknown account from a wrong password — do not surface a more specific message to the caller. |
| `403` | ACCOUNT_LOCKED | Account is locked. Please contact support. | The account has been locked, typically after repeated failed attempts. | An operator must unlock it. Retrying does not help. |

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

#### See also

- `POST /user/refresh`
- `POST /signin/passcode`

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `orgid` | header | string | — | The org to sign into. Omit it to let the email find the org (see the description). |
| `x-client-info` | header | string | — | Client context recorded against the attempt — used for device tracking and blocking. |

### Request body

Credentials.

```json
{
  "email": "ada@example.com",
  "password": "correct horse battery staple"
}
```

### Responses

| Status | Meaning |
| --- | --- |
| `201` | A session, or the next step to take |
| `400` | Invalid username or password — The identifier or password is wrong. |
| `403` | Account is locked. Please contact support. — The account has been locked, typically after repeated failed attempts. |
| `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
{
  "token": "eyJhbGciOi…",
  "refreshToken": "eyJhbGciOi…",
  "orgId": "acme",
  "user": {
    "sk": "65f0c2a1e4b0a1b2c3d4e5f6",
    "data": {
      "email": "ada@example.com"
    }
  }
}
```

## POST /profile/user/signin/passcode

**Sign in with a passcode**

`operationId: UsersController_signinPasscode`

Authenticates with a numeric passcode rather than a password — the shop-floor and shared-terminal flow, where typing a long password on a till is impractical.

#### Signature

```http
POST /profile/user/signin/passcode (body) -> Tokens and the signed-in user
```

#### Access

Public — no credentials required.

#### Notes

- A six-digit passcode is far weaker than a password — pair it with device restrictions.

#### Errors

| Status | Code | Message | When | What to do |
| --- | --- | --- | --- | --- |
| `400` | INVALID_CREDENTIALS | Invalid username or password | The identifier or password is wrong. | Deliberately does not distinguish an unknown account from a wrong password — do not surface a more specific message to the caller. |
| `403` | ACCOUNT_LOCKED | Account is locked. Please contact support. | The account has been locked, typically after repeated failed attempts. | An operator must unlock it. Retrying does not help. |

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

#### See also

- `POST /passcode/set`

### 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. |
| `x-client-info` | header | string | — | Client context recorded against the attempt — used for device tracking and blocking. |

### Request body

Passcode credentials.

```json
{
  "employeeId": "E-00412",
  "passcode": "481625"
}
```

### Responses

| Status | Meaning |
| --- | --- |
| `201` | Tokens and the signed-in user |
| `400` | Invalid username or password — The identifier or password is wrong. |
| `403` | Account is locked. Please contact support. — The account has been locked, typically after repeated failed attempts. |
| `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/signin/passcode

**Sign in with a passcode**

`operationId: UsersController_signinPasscode`

Authenticates with a numeric passcode rather than a password — the shop-floor and shared-terminal flow, where typing a long password on a till is impractical.

#### Signature

```http
POST /profile/signin/passcode (body) -> Tokens and the signed-in user
```

#### Access

Public — no credentials required.

#### Notes

- A six-digit passcode is far weaker than a password — pair it with device restrictions.

#### Errors

| Status | Code | Message | When | What to do |
| --- | --- | --- | --- | --- |
| `400` | INVALID_CREDENTIALS | Invalid username or password | The identifier or password is wrong. | Deliberately does not distinguish an unknown account from a wrong password — do not surface a more specific message to the caller. |
| `403` | ACCOUNT_LOCKED | Account is locked. Please contact support. | The account has been locked, typically after repeated failed attempts. | An operator must unlock it. Retrying does not help. |

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

#### See also

- `POST /passcode/set`

### 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. |
| `x-client-info` | header | string | — | Client context recorded against the attempt — used for device tracking and blocking. |

### Request body

Passcode credentials.

```json
{
  "employeeId": "E-00412",
  "passcode": "481625"
}
```

### Responses

| Status | Meaning |
| --- | --- |
| `201` | Tokens and the signed-in user |
| `400` | Invalid username or password — The identifier or password is wrong. |
| `403` | Account is locked. Please contact support. — The account has been locked, typically after repeated failed attempts. |
| `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/signin/passcode

**Sign in with a passcode**

`operationId: UsersController_signinPasscode`

Authenticates with a numeric passcode rather than a password — the shop-floor and shared-terminal flow, where typing a long password on a till is impractical.

#### Signature

```http
POST /user/user/signin/passcode (body) -> Tokens and the signed-in user
```

#### Access

Public — no credentials required.

#### Notes

- A six-digit passcode is far weaker than a password — pair it with device restrictions.

#### Errors

| Status | Code | Message | When | What to do |
| --- | --- | --- | --- | --- |
| `400` | INVALID_CREDENTIALS | Invalid username or password | The identifier or password is wrong. | Deliberately does not distinguish an unknown account from a wrong password — do not surface a more specific message to the caller. |
| `403` | ACCOUNT_LOCKED | Account is locked. Please contact support. | The account has been locked, typically after repeated failed attempts. | An operator must unlock it. Retrying does not help. |

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

#### See also

- `POST /passcode/set`

### 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. |
| `x-client-info` | header | string | — | Client context recorded against the attempt — used for device tracking and blocking. |

### Request body

Passcode credentials.

```json
{
  "employeeId": "E-00412",
  "passcode": "481625"
}
```

### Responses

| Status | Meaning |
| --- | --- |
| `201` | Tokens and the signed-in user |
| `400` | Invalid username or password — The identifier or password is wrong. |
| `403` | Account is locked. Please contact support. — The account has been locked, typically after repeated failed attempts. |
| `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/signin/passcode

**Sign in with a passcode**

`operationId: UsersController_signinPasscode`

Authenticates with a numeric passcode rather than a password — the shop-floor and shared-terminal flow, where typing a long password on a till is impractical.

#### Signature

```http
POST /user/signin/passcode (body) -> Tokens and the signed-in user
```

#### Access

Public — no credentials required.

#### Notes

- A six-digit passcode is far weaker than a password — pair it with device restrictions.
- Same as POST /signin/passcode: `surface: "pos"` applies the POS readiness gate.
- Workforce Readiness: with no requirement rules this endpoint behaves exactly as before. `warn` never stops anything — it comes back as `readiness.warnings` in the response.

#### Errors

| Status | Code | Message | When | What to do |
| --- | --- | --- | --- | --- |
| `400` | INVALID_CREDENTIALS | Invalid username or password | The identifier or password is wrong. | Deliberately does not distinguish an unknown account from a wrong password — do not surface a more specific message to the caller. |
| `403` | ACCOUNT_LOCKED | Account is locked. Please contact support. | The account has been locked, typically after repeated failed attempts. | An operator must unlock it. Retrying does not help. |
| `423` | READINESS_BLOCK | <name> can't sign in to the POS: <requirement titles>. | A requirement rule whose `enforcement.pos` effect is block (or override-with-reason) is unmet for this person, and no override is active. | Show `message` and `reasons`. A location manager or HR can resend the same request with `override: { reasonCode, reason, expiresAt }` — it is written to the readiness ledger with the approver and expiry. The person’s own override is refused. |

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

#### See also

- `POST /passcode/set`

### 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. |
| `x-client-info` | header | string | — | Client context recorded against the attempt — used for device tracking and blocking. |

### Request body

Passcode credentials.

```json
{
  "employeeId": "E-00412",
  "passcode": "481625"
}
```

### Responses

| Status | Meaning |
| --- | --- |
| `201` | Tokens and the signed-in user |
| `400` | Invalid username or password — The identifier or password is wrong. |
| `403` | Account is locked. Please contact support. — The account has been locked, typically after repeated failed attempts. |
| `423` | <name> can't sign in to the POS: <requirement titles>. — A requirement rule whose `enforcement.pos` effect is block (or override-with-reason) is unmet for this person, and no override is active. |
| `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/passcode/set

**Set a passcode**

`operationId: UsersController_setPasscode`

Sets a numeric passcode for terminal sign-in. **Must be exactly six digits** — the check is strict, so a shorter or non-numeric value is refused.

#### Signature

```http
POST /profile/passcode/set (body) -> The result
```

#### Access

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

#### Errors

| Status | Code | Message | When | What to do |
| --- | --- | --- | --- | --- |
| `400` | INVALID_PASSCODE | Passcode must be exactly 6 digits | The passcode is not six digits. | Exactly six numeric digits. |

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

#### See also

- `POST /signin/passcode`

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

```json
{
  "employeeId": "E-00412",
  "passcode": "481625"
}
```

### Responses

| Status | Meaning |
| --- | --- |
| `201` | The result |
| `400` | Passcode must be exactly 6 digits — The passcode is not six digits. |
| `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/passcode/set

**Set a passcode**

`operationId: UsersController_setPasscode`

Sets a numeric passcode for terminal sign-in. **Must be exactly six digits** — the check is strict, so a shorter or non-numeric value is refused.

#### Signature

```http
POST /profile/user/passcode/set (body) -> The result
```

#### Access

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

#### Errors

| Status | Code | Message | When | What to do |
| --- | --- | --- | --- | --- |
| `400` | INVALID_PASSCODE | Passcode must be exactly 6 digits | The passcode is not six digits. | Exactly six numeric digits. |

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

#### See also

- `POST /signin/passcode`

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

```json
{
  "employeeId": "E-00412",
  "passcode": "481625"
}
```

### Responses

| Status | Meaning |
| --- | --- |
| `201` | The result |
| `400` | Passcode must be exactly 6 digits — The passcode is not six digits. |
| `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/passcode/set

**Set a passcode**

`operationId: UsersController_setPasscode`

Sets a numeric passcode for terminal sign-in. **Must be exactly six digits** — the check is strict, so a shorter or non-numeric value is refused.

#### Signature

```http
POST /user/passcode/set (body) -> The result
```

#### Access

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

#### Errors

| Status | Code | Message | When | What to do |
| --- | --- | --- | --- | --- |
| `400` | INVALID_PASSCODE | Passcode must be exactly 6 digits | The passcode is not six digits. | Exactly six numeric digits. |

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

#### See also

- `POST /signin/passcode`

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

```json
{
  "employeeId": "E-00412",
  "passcode": "481625"
}
```

### Responses

| Status | Meaning |
| --- | --- |
| `201` | The result |
| `400` | Passcode must be exactly 6 digits — The passcode is not six digits. |
| `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/passcode/set

**Set a passcode**

`operationId: UsersController_setPasscode`

Sets a numeric passcode for terminal sign-in. **Must be exactly six digits** — the check is strict, so a shorter or non-numeric value is refused.

#### Signature

```http
POST /user/user/passcode/set (body) -> The result
```

#### Access

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

#### Errors

| Status | Code | Message | When | What to do |
| --- | --- | --- | --- | --- |
| `400` | INVALID_PASSCODE | Passcode must be exactly 6 digits | The passcode is not six digits. | Exactly six numeric digits. |

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

#### See also

- `POST /signin/passcode`

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

```json
{
  "employeeId": "E-00412",
  "passcode": "481625"
}
```

### Responses

| Status | Meaning |
| --- | --- |
| `201` | The result |
| `400` | Passcode must be exactly 6 digits — The passcode is not six digits. |
| `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/passcode/remove

**Remove a passcode**

`operationId: UsersController_removePasscode`

Removes a passcode, disabling terminal sign-in for that employee. Do this as part of offboarding.

#### Signature

```http
POST /profile/passcode/remove (body) -> The result
```

#### Access

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

#### Errors

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

#### See also

- `POST /business-made/offboarding/{id}/revoke-access`

### 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 passcode to remove.

```json
{
  "employeeId": "E-00412"
}
```

### 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. |
| `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/passcode/remove

**Remove a passcode**

`operationId: UsersController_removePasscode`

Removes a passcode, disabling terminal sign-in for that employee. Do this as part of offboarding.

#### Signature

```http
POST /profile/user/passcode/remove (body) -> The result
```

#### Access

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

#### Errors

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

#### See also

- `POST /business-made/offboarding/{id}/revoke-access`

### 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 passcode to remove.

```json
{
  "employeeId": "E-00412"
}
```

### 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. |
| `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/passcode/remove

**Remove a passcode**

`operationId: UsersController_removePasscode`

Removes a passcode, disabling terminal sign-in for that employee. Do this as part of offboarding.

#### Signature

```http
POST /user/passcode/remove (body) -> The result
```

#### Access

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

#### Errors

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

#### See also

- `POST /business-made/offboarding/{id}/revoke-access`

### 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 passcode to remove.

```json
{
  "employeeId": "E-00412"
}
```

### 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. |
| `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/passcode/remove

**Remove a passcode**

`operationId: UsersController_removePasscode`

Removes a passcode, disabling terminal sign-in for that employee. Do this as part of offboarding.

#### Signature

```http
POST /user/user/passcode/remove (body) -> The result
```

#### Access

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

#### Errors

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

#### See also

- `POST /business-made/offboarding/{id}/revoke-access`

### 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 passcode to remove.

```json
{
  "employeeId": "E-00412"
}
```

### 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. |
| `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/passcode/card/register

**Register a passcode card**

`operationId: UsersController_registerCard`

Registers a physical card or fob for terminal sign-in.

#### Signature

```http
POST /profile/passcode/card/register (body) -> The registered card
```

#### Access

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

#### Errors

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

#### See also

- `POST /passcode/card/status`

### 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 card to register.

```json
{
  "employeeId": "E-00412",
  "cardIdentifier": "CARD-9K2M4H"
}
```

### Responses

| Status | Meaning |
| --- | --- |
| `201` | The registered card |
| `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/passcode/card/register

**Register a passcode card**

`operationId: UsersController_registerCard`

Registers a physical card or fob for terminal sign-in.

#### Signature

```http
POST /profile/user/passcode/card/register (body) -> The registered card
```

#### Access

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

#### Errors

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

#### See also

- `POST /passcode/card/status`

### 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 card to register.

```json
{
  "employeeId": "E-00412",
  "cardIdentifier": "CARD-9K2M4H"
}
```

### Responses

| Status | Meaning |
| --- | --- |
| `201` | The registered card |
| `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/passcode/card/register

**Register a passcode card**

`operationId: UsersController_registerCard`

Registers a physical card or fob for terminal sign-in.

#### Signature

```http
POST /user/passcode/card/register (body) -> The registered card
```

#### Access

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

#### Errors

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

#### See also

- `POST /passcode/card/status`

### 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 card to register.

```json
{
  "employeeId": "E-00412",
  "cardIdentifier": "CARD-9K2M4H"
}
```

### Responses

| Status | Meaning |
| --- | --- |
| `201` | The registered card |
| `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/passcode/card/register

**Register a passcode card**

`operationId: UsersController_registerCard`

Registers a physical card or fob for terminal sign-in.

#### Signature

```http
POST /user/user/passcode/card/register (body) -> The registered card
```

#### Access

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

#### Errors

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

#### See also

- `POST /passcode/card/status`

### 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 card to register.

```json
{
  "employeeId": "E-00412",
  "cardIdentifier": "CARD-9K2M4H"
}
```

### Responses

| Status | Meaning |
| --- | --- |
| `201` | The registered card |
| `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/passcode/card/status

**Set a card status**

`operationId: UsersController_setCardStatus`

Enables or disables a registered card. Disabling is the immediate response to a lost card — faster and more reversible than deleting it.

#### Signature

```http
POST /profile/passcode/card/status (body) -> The updated card
```

#### Access

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

#### Errors

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

#### See also

- `GET /passcode/card/{identifier}`

### 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 card and its new status.

```json
{
  "cardIdentifier": "CARD-9K2M4H",
  "status": "disabled"
}
```

### Responses

| Status | Meaning |
| --- | --- |
| `201` | The updated card |
| `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/passcode/card/status

**Set a card status**

`operationId: UsersController_setCardStatus`

Enables or disables a registered card. Disabling is the immediate response to a lost card — faster and more reversible than deleting it.

#### Signature

```http
POST /profile/user/passcode/card/status (body) -> The updated card
```

#### Access

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

#### Errors

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

#### See also

- `GET /passcode/card/{identifier}`

### 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 card and its new status.

```json
{
  "cardIdentifier": "CARD-9K2M4H",
  "status": "disabled"
}
```

### Responses

| Status | Meaning |
| --- | --- |
| `201` | The updated card |
| `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/passcode/card/status

**Set a card status**

`operationId: UsersController_setCardStatus`

Enables or disables a registered card. Disabling is the immediate response to a lost card — faster and more reversible than deleting it.

#### Signature

```http
POST /user/passcode/card/status (body) -> The updated card
```

#### Access

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

#### Errors

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

#### See also

- `GET /passcode/card/{identifier}`

### 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 card and its new status.

```json
{
  "cardIdentifier": "CARD-9K2M4H",
  "status": "disabled"
}
```

### Responses

| Status | Meaning |
| --- | --- |
| `201` | The updated card |
| `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/passcode/card/status

**Set a card status**

`operationId: UsersController_setCardStatus`

Enables or disables a registered card. Disabling is the immediate response to a lost card — faster and more reversible than deleting it.

#### Signature

```http
POST /user/user/passcode/card/status (body) -> The updated card
```

#### Access

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

#### Errors

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

#### See also

- `GET /passcode/card/{identifier}`

### 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 card and its new status.

```json
{
  "cardIdentifier": "CARD-9K2M4H",
  "status": "disabled"
}
```

### Responses

| Status | Meaning |
| --- | --- |
| `201` | The updated card |
| `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/passcode/cards/{employeeId}

**List an employee's cards**

`operationId: UsersController_listCards`

The cards registered to an employee. Omitting the segment lists cards more broadly.

#### Signature

```http
GET /profile/passcode/cards/{employeeId} (employeeId: string) -> Registered cards
```

#### Access

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

#### Errors

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

#### See also

- `POST /passcode/card/register`

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

### Responses

| Status | Meaning |
| --- | --- |
| `200` | Registered cards |
| `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/passcode/cards/{employeeId}

**List an employee's cards**

`operationId: UsersController_listCards`

The cards registered to an employee. Omitting the segment lists cards more broadly.

#### Signature

```http
GET /profile/user/passcode/cards/{employeeId} (employeeId: string) -> Registered cards
```

#### Access

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

#### Errors

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

#### See also

- `POST /passcode/card/register`

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

### Responses

| Status | Meaning |
| --- | --- |
| `200` | Registered cards |
| `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/passcode/cards/{employeeId}

**List an employee's cards**

`operationId: UsersController_listCards`

The cards registered to an employee. Omitting the segment lists cards more broadly.

#### Signature

```http
GET /user/passcode/cards/{employeeId} (employeeId: string) -> Registered cards
```

#### Access

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

#### Errors

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

#### See also

- `POST /passcode/card/register`

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

### Responses

| Status | Meaning |
| --- | --- |
| `200` | Registered cards |
| `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/passcode/cards/{employeeId}

**List an employee's cards**

`operationId: UsersController_listCards`

The cards registered to an employee. Omitting the segment lists cards more broadly.

#### Signature

```http
GET /user/user/passcode/cards/{employeeId} (employeeId: string) -> Registered cards
```

#### Access

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

#### Errors

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

#### See also

- `POST /passcode/card/register`

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

### Responses

| Status | Meaning |
| --- | --- |
| `200` | Registered cards |
| `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/passcode/card/{identifier}

**Get card information**

`operationId: UsersController_getPasscodeCardInfo`

Looks up a registered card by its identifier — what a terminal does when a card is presented.

#### Signature

```http
GET /profile/passcode/card/{identifier} (identifier: string) -> The card
```

#### Access

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

#### Errors

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

#### See also

- `GET /passcode/cards/{employeeId}`

### 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. |
| `identifier` | path | string | yes | Card identifier. |

### Responses

| Status | Meaning |
| --- | --- |
| `200` | The card |
| `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/passcode/card/{identifier}

**Get card information**

`operationId: UsersController_getPasscodeCardInfo`

Looks up a registered card by its identifier — what a terminal does when a card is presented.

#### Signature

```http
GET /profile/user/passcode/card/{identifier} (identifier: string) -> The card
```

#### Access

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

#### Errors

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

#### See also

- `GET /passcode/cards/{employeeId}`

### 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. |
| `identifier` | path | string | yes | Card identifier. |

### Responses

| Status | Meaning |
| --- | --- |
| `200` | The card |
| `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/passcode/card/{identifier}

**Get card information**

`operationId: UsersController_getPasscodeCardInfo`

Looks up a registered card by its identifier — what a terminal does when a card is presented.

#### Signature

```http
GET /user/passcode/card/{identifier} (identifier: string) -> The card
```

#### Access

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

#### Errors

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

#### See also

- `GET /passcode/cards/{employeeId}`

### 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. |
| `identifier` | path | string | yes | Card identifier. |

### Responses

| Status | Meaning |
| --- | --- |
| `200` | The card |
| `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/passcode/card/{identifier}

**Get card information**

`operationId: UsersController_getPasscodeCardInfo`

Looks up a registered card by its identifier — what a terminal does when a card is presented.

#### Signature

```http
GET /user/user/passcode/card/{identifier} (identifier: string) -> The card
```

#### Access

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

#### Errors

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

#### See also

- `GET /passcode/cards/{employeeId}`

### 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. |
| `identifier` | path | string | yes | Card identifier. |

### Responses

| Status | Meaning |
| --- | --- |
| `200` | The card |
| `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/refresh

**Refresh an access token**

`operationId: UsersController_refreshToken`

Exchanges a refresh token for a new access token. The refresh token is longer-lived and therefore the more valuable credential — store it more carefully than the access token.

#### Signature

```http
POST /profile/user/refresh (body) -> New tokens
```

#### Access

Public — no credentials required.

#### Errors

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

#### See also

- `POST /signin`

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

```json
{
  "refreshToken": "eyJhbGciOi…"
}
```

### Responses

| Status | Meaning |
| --- | --- |
| `201` | New tokens |
| `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/refresh

**Refresh an access token**

`operationId: UsersController_refreshToken`

Exchanges a refresh token for a new access token. The refresh token is longer-lived and therefore the more valuable credential — store it more carefully than the access token.

#### Signature

```http
POST /user/user/refresh (body) -> New tokens
```

#### Access

Public — no credentials required.

#### Errors

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

#### See also

- `POST /signin`

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

```json
{
  "refreshToken": "eyJhbGciOi…"
}
```

### Responses

| Status | Meaning |
| --- | --- |
| `201` | New tokens |
| `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/signup

**Sign up**

`operationId: UsersController_signup`

Registers a new user in the organization. An address already registered is refused rather than silently creating a duplicate.

#### Signature

```http
POST /profile/signup (body) -> The created user, usually with tokens
```

#### Access

Public — no credentials required.

#### Notes

- The error confirms whether an address is registered, unlike sign-in. That is an enumeration surface — rate-limit it.

#### Errors

| Status | Code | Message | When | What to do |
| --- | --- | --- | --- | --- |
| `400` | EMAIL_EXISTS | A user with this email already exists in the organization | The email is already registered. | Sign in instead, or use the password reset flow. |

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

#### See also

- `POST /signin`
- `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 account to create.

```json
{
  "email": "ada@example.com",
  "password": "correct horse battery staple",
  "firstName": "Ada",
  "lastName": "Lovelace"
}
```

### Responses

| Status | Meaning |
| --- | --- |
| `201` | The created user, usually with tokens |
| `400` | A user with this email already exists in the organization — The email is already registered. |
| `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/signup

**Sign up**

`operationId: UsersController_signup`

Registers a new user in the organization. An address already registered is refused rather than silently creating a duplicate.

#### Signature

```http
POST /profile/user/signup (body) -> The created user, usually with tokens
```

#### Access

Public — no credentials required.

#### Notes

- The error confirms whether an address is registered, unlike sign-in. That is an enumeration surface — rate-limit it.

#### Errors

| Status | Code | Message | When | What to do |
| --- | --- | --- | --- | --- |
| `400` | EMAIL_EXISTS | A user with this email already exists in the organization | The email is already registered. | Sign in instead, or use the password reset flow. |

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

#### See also

- `POST /signin`
- `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 account to create.

```json
{
  "email": "ada@example.com",
  "password": "correct horse battery staple",
  "firstName": "Ada",
  "lastName": "Lovelace"
}
```

### Responses

| Status | Meaning |
| --- | --- |
| `201` | The created user, usually with tokens |
| `400` | A user with this email already exists in the organization — The email is already registered. |
| `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/signup

**Sign up**

`operationId: UsersController_signup`

Registers a new user in the organization. An address already registered is refused rather than silently creating a duplicate.

#### Signature

```http
POST /user/signup (body) -> The created user, usually with tokens
```

#### Access

Public — no credentials required.

#### Notes

- The error confirms whether an address is registered, unlike sign-in. That is an enumeration surface — rate-limit it.

#### Errors

| Status | Code | Message | When | What to do |
| --- | --- | --- | --- | --- |
| `400` | EMAIL_EXISTS | A user with this email already exists in the organization | The email is already registered. | Sign in instead, or use the password reset flow. |

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

#### See also

- `POST /signin`
- `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 account to create.

```json
{
  "email": "ada@example.com",
  "password": "correct horse battery staple",
  "firstName": "Ada",
  "lastName": "Lovelace"
}
```

### Responses

| Status | Meaning |
| --- | --- |
| `201` | The created user, usually with tokens |
| `400` | A user with this email already exists in the organization — The email is already registered. |
| `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/signup

**Sign up**

`operationId: UsersController_signup`

Registers a new user in the organization. An address already registered is refused rather than silently creating a duplicate.

#### Signature

```http
POST /user/user/signup (body) -> The created user, usually with tokens
```

#### Access

Public — no credentials required.

#### Notes

- The error confirms whether an address is registered, unlike sign-in. That is an enumeration surface — rate-limit it.

#### Errors

| Status | Code | Message | When | What to do |
| --- | --- | --- | --- | --- |
| `400` | EMAIL_EXISTS | A user with this email already exists in the organization | The email is already registered. | Sign in instead, or use the password reset flow. |

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

#### See also

- `POST /signin`
- `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 account to create.

```json
{
  "email": "ada@example.com",
  "password": "correct horse battery staple",
  "firstName": "Ada",
  "lastName": "Lovelace"
}
```

### Responses

| Status | Meaning |
| --- | --- |
| `201` | The created user, usually with tokens |
| `400` | A user with this email already exists in the organization — The email is already registered. |
| `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/logout/{userId}

**Log a user out**

`operationId: UsersController_logout`

Ends a user's session by id. Note this is a `GET` that changes state, so it can be triggered by anything following a link.

#### Signature

```http
GET /profile/logout/{userId} (userId: string) -> Logout result
```

#### Access

Public — no credentials required.

#### Notes

- State change on `GET` — do not place behind a prefetchable link.

#### Errors

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

#### See also

- `POST /signout`

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

### Responses

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

## GET /profile/user/logout/{userId}

**Log a user out**

`operationId: UsersController_logout`

Ends a user's session by id. Note this is a `GET` that changes state, so it can be triggered by anything following a link.

#### Signature

```http
GET /profile/user/logout/{userId} (userId: string) -> Logout result
```

#### Access

Public — no credentials required.

#### Notes

- State change on `GET` — do not place behind a prefetchable link.

#### Errors

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

#### See also

- `POST /signout`

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

### Responses

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

## GET /user/logout/{userId}

**Log a user out**

`operationId: UsersController_logout`

Ends a user's session by id. Note this is a `GET` that changes state, so it can be triggered by anything following a link.

#### Signature

```http
GET /user/logout/{userId} (userId: string) -> Logout result
```

#### Access

Public — no credentials required.

#### Notes

- State change on `GET` — do not place behind a prefetchable link.

#### Errors

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

#### See also

- `POST /signout`

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

### Responses

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

## GET /user/user/logout/{userId}

**Log a user out**

`operationId: UsersController_logout`

Ends a user's session by id. Note this is a `GET` that changes state, so it can be triggered by anything following a link.

#### Signature

```http
GET /user/user/logout/{userId} (userId: string) -> Logout result
```

#### Access

Public — no credentials required.

#### Notes

- State change on `GET` — do not place behind a prefetchable link.

#### Errors

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

#### See also

- `POST /signout`

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

### Responses

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

## POST /profile/password/change

**Change a password**

`operationId: UsersController_passwordChange`

Any signed-in person may call it. **Below ConfigAdmin** you change your own password only: a `userId` other than your own `sk` is refused (403); omit it to mean yourself. Your current password is required unless the account is on a temporary password. The password policy that governs you (see *Password policies* below) must allow self-service change — except a temporary-password account finishing its forced first change, which is always allowed — and the new password must meet that policy's length and complexity rules. **ConfigAdmin and above** change anyone's password as before, without the policy switch. Success clears `temporaryPassword`.

#### Signature

```http
POST /profile/password/change (body) -> The updated user
```

#### Access

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

#### Notes

- Password policies: a `passwordpolicy` record carries `allowSelfServiceChange` / `allowSelfServiceReset` (both default on) and the rules (`minLenght`, `minLowercase`, `minUppercase`, `minNumbers`, `minSymbols`, `exclude`, `pattern`). A user, group or role points at one through `data.passwordPolicy` (the policy name); a policy with `isDefault: true` covers everyone else. Precedence: user > group > role > org default; within a level the first by name. **No policy applies = full self-service, no rules.**

#### Errors

| Status | Code | Message | When | What to do |
| --- | --- | --- | --- | --- |
| `400` | INVALID_PASSWORD | password not correct | The current password is wrong. | The change is refused; the existing password stands. |
| `403` | NOT_YOURS | You can only change your own password | A non-admin named someone else's `userId`. | Ask an administrator. |

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

#### See also

- `POST /password/reset`
- `GET /user/password-policy/assignments`

### 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. |
| `x-client-info` | header | string | — | Client context recorded against the attempt — used for device tracking and blocking. |

### Request body

Target (optional, `sk`), current and new password.

```json
{
  "userId": "65f0c2a1e4b0a1b2c3d4e5f6",
  "password": "…",
  "newPassword": "…"
}
```

### Responses

| Status | Meaning |
| --- | --- |
| `201` | The updated user |
| `400` | password not correct — The current password is wrong. |
| `401` | Authentication failed: Invalid or expired token — The `Authorization` header is missing, malformed, or the JWT has expired. |
| `403` | You can only change your own password — A non-admin named someone else's `userId`. |
| `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/password/change

**Change a password**

`operationId: UsersController_passwordChange`

Any signed-in person may call it. **Below ConfigAdmin** you change your own password only: a `userId` other than your own `sk` is refused (403); omit it to mean yourself. Your current password is required unless the account is on a temporary password. The password policy that governs you (see *Password policies* below) must allow self-service change — except a temporary-password account finishing its forced first change, which is always allowed — and the new password must meet that policy's length and complexity rules. **ConfigAdmin and above** change anyone's password as before, without the policy switch. Success clears `temporaryPassword`.

#### Signature

```http
POST /profile/user/password/change (body) -> The updated user
```

#### Access

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

#### Notes

- Password policies: a `passwordpolicy` record carries `allowSelfServiceChange` / `allowSelfServiceReset` (both default on) and the rules (`minLenght`, `minLowercase`, `minUppercase`, `minNumbers`, `minSymbols`, `exclude`, `pattern`). A user, group or role points at one through `data.passwordPolicy` (the policy name); a policy with `isDefault: true` covers everyone else. Precedence: user > group > role > org default; within a level the first by name. **No policy applies = full self-service, no rules.**

#### Errors

| Status | Code | Message | When | What to do |
| --- | --- | --- | --- | --- |
| `400` | INVALID_PASSWORD | password not correct | The current password is wrong. | The change is refused; the existing password stands. |
| `403` | NOT_YOURS | You can only change your own password | A non-admin named someone else's `userId`. | Ask an administrator. |

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

#### See also

- `POST /password/reset`
- `GET /user/password-policy/assignments`

### 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. |
| `x-client-info` | header | string | — | Client context recorded against the attempt — used for device tracking and blocking. |

### Request body

Target (optional, `sk`), current and new password.

```json
{
  "userId": "65f0c2a1e4b0a1b2c3d4e5f6",
  "password": "…",
  "newPassword": "…"
}
```

### Responses

| Status | Meaning |
| --- | --- |
| `201` | The updated user |
| `400` | password not correct — The current password is wrong. |
| `401` | Authentication failed: Invalid or expired token — The `Authorization` header is missing, malformed, or the JWT has expired. |
| `403` | You can only change your own password — A non-admin named someone else's `userId`. |
| `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/password/change

**Change a password**

`operationId: UsersController_passwordChange`

Any signed-in person may call it. **Below ConfigAdmin** you change your own password only: a `userId` other than your own `sk` is refused (403); omit it to mean yourself. Your current password is required unless the account is on a temporary password. The password policy that governs you (see *Password policies* below) must allow self-service change — except a temporary-password account finishing its forced first change, which is always allowed — and the new password must meet that policy's length and complexity rules. **ConfigAdmin and above** change anyone's password as before, without the policy switch. Success clears `temporaryPassword`.

#### Signature

```http
POST /user/password/change (body) -> The updated user
```

#### Access

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

#### Notes

- Password policies: a `passwordpolicy` record carries `allowSelfServiceChange` / `allowSelfServiceReset` (both default on) and the rules (`minLenght`, `minLowercase`, `minUppercase`, `minNumbers`, `minSymbols`, `exclude`, `pattern`). A user, group or role points at one through `data.passwordPolicy` (the policy name); a policy with `isDefault: true` covers everyone else. Precedence: user > group > role > org default; within a level the first by name. **No policy applies = full self-service, no rules.**

#### Errors

| Status | Code | Message | When | What to do |
| --- | --- | --- | --- | --- |
| `400` | INVALID_PASSWORD | password not correct | The current password is wrong. | The change is refused; the existing password stands. |
| `403` | NOT_YOURS | You can only change your own password | A non-admin named someone else's `userId`. | Ask an administrator. |

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

#### See also

- `POST /password/reset`
- `GET /user/password-policy/assignments`

### 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. |
| `x-client-info` | header | string | — | Client context recorded against the attempt — used for device tracking and blocking. |

### Request body

Target (optional, `sk`), current and new password.

```json
{
  "userId": "65f0c2a1e4b0a1b2c3d4e5f6",
  "password": "…",
  "newPassword": "…"
}
```

### Responses

| Status | Meaning |
| --- | --- |
| `201` | The updated user |
| `400` | password not correct — The current password is wrong. |
| `401` | Authentication failed: Invalid or expired token — The `Authorization` header is missing, malformed, or the JWT has expired. |
| `403` | You can only change your own password — A non-admin named someone else's `userId`. |
| `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/password/change

**Change a password**

`operationId: UsersController_passwordChange`

Any signed-in person may call it. **Below ConfigAdmin** you change your own password only: a `userId` other than your own `sk` is refused (403); omit it to mean yourself. Your current password is required unless the account is on a temporary password. The password policy that governs you (see *Password policies* below) must allow self-service change — except a temporary-password account finishing its forced first change, which is always allowed — and the new password must meet that policy's length and complexity rules. **ConfigAdmin and above** change anyone's password as before, without the policy switch. Success clears `temporaryPassword`.

#### Signature

```http
POST /user/user/password/change (body) -> The updated user
```

#### Access

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

#### Notes

- Password policies: a `passwordpolicy` record carries `allowSelfServiceChange` / `allowSelfServiceReset` (both default on) and the rules (`minLenght`, `minLowercase`, `minUppercase`, `minNumbers`, `minSymbols`, `exclude`, `pattern`). A user, group or role points at one through `data.passwordPolicy` (the policy name); a policy with `isDefault: true` covers everyone else. Precedence: user > group > role > org default; within a level the first by name. **No policy applies = full self-service, no rules.**

#### Errors

| Status | Code | Message | When | What to do |
| --- | --- | --- | --- | --- |
| `400` | INVALID_PASSWORD | password not correct | The current password is wrong. | The change is refused; the existing password stands. |
| `403` | NOT_YOURS | You can only change your own password | A non-admin named someone else's `userId`. | Ask an administrator. |

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

#### See also

- `POST /password/reset`
- `GET /user/password-policy/assignments`

### 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. |
| `x-client-info` | header | string | — | Client context recorded against the attempt — used for device tracking and blocking. |

### Request body

Target (optional, `sk`), current and new password.

```json
{
  "userId": "65f0c2a1e4b0a1b2c3d4e5f6",
  "password": "…",
  "newPassword": "…"
}
```

### Responses

| Status | Meaning |
| --- | --- |
| `201` | The updated user |
| `400` | password not correct — The current password is wrong. |
| `401` | Authentication failed: Invalid or expired token — The `Authorization` header is missing, malformed, or the JWT has expired. |
| `403` | You can only change your own password — A non-admin named someone else's `userId`. |
| `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/password-policy/assignments

**Password policies and who they apply to**

`operationId: UsersController_passwordPolicyAssignments`

Every password policy with the people, groups and roles that carry it (`data.passwordPolicy`), whether it is the org default, and its self-service switches — plus `options` (all users, groups, roles with their current policy) for assigning one. Precedence when several apply: user > group > role > org default.

#### Signature

```http
GET /profile/user/password-policy/assignments () -> { precedence, policies[], options }
```

#### Access

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

#### Notes

- ConfigAdmin.

#### Errors

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

#### See also

- `POST /user/password-policy/assign`

### 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` | { precedence, policies[], options } |
| `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/password-policy/assignments

**Password policies and who they apply to**

`operationId: UsersController_passwordPolicyAssignments`

Every password policy with the people, groups and roles that carry it (`data.passwordPolicy`), whether it is the org default, and its self-service switches — plus `options` (all users, groups, roles with their current policy) for assigning one. Precedence when several apply: user > group > role > org default.

#### Signature

```http
GET /user/user/password-policy/assignments () -> { precedence, policies[], options }
```

#### Access

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

#### Notes

- ConfigAdmin.

#### Errors

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

#### See also

- `POST /user/password-policy/assign`

### 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` | { precedence, policies[], options } |
| `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/password-policy/assign

**Assign a password policy**

`operationId: UsersController_assignPasswordPolicy`

Puts a policy on one user, group or role (sets its `data.passwordPolicy` to the policy name), or clears it with an empty `policy`. Returns the refreshed assignments.

#### Signature

```http
POST /profile/user/password-policy/assign (body) -> Same shape as the assignments listing
```

#### Access

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

#### Notes

- ConfigAdmin. Root groups and roles can only be changed from the root organisation.

#### Errors

| Status | Code | Message | When | What to do |
| --- | --- | --- | --- | --- |
| `404` | NOT_FOUND | No password policy named floor-staff | The policy or the target does not exist. | Check the name / sk. |

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

#### See also

- `GET /user/password-policy/assignments`

### 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 assign to, and which policy.

```json
{
  "kind": "group",
  "target": "65f0c2a1e4b0a1b2c3d4e5f6",
  "policy": "floor-staff"
}
```

### Responses

| Status | Meaning |
| --- | --- |
| `201` | Same shape as the assignments listing |
| `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 password policy named floor-staff — The policy or the target does not exist. |
| `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/password-policy/assign

**Assign a password policy**

`operationId: UsersController_assignPasswordPolicy`

Puts a policy on one user, group or role (sets its `data.passwordPolicy` to the policy name), or clears it with an empty `policy`. Returns the refreshed assignments.

#### Signature

```http
POST /user/user/password-policy/assign (body) -> Same shape as the assignments listing
```

#### Access

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

#### Notes

- ConfigAdmin. Root groups and roles can only be changed from the root organisation.

#### Errors

| Status | Code | Message | When | What to do |
| --- | --- | --- | --- | --- |
| `404` | NOT_FOUND | No password policy named floor-staff | The policy or the target does not exist. | Check the name / sk. |

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

#### See also

- `GET /user/password-policy/assignments`

### 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 assign to, and which policy.

```json
{
  "kind": "group",
  "target": "65f0c2a1e4b0a1b2c3d4e5f6",
  "policy": "floor-staff"
}
```

### Responses

| Status | Meaning |
| --- | --- |
| `201` | Same shape as the assignments listing |
| `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 password policy named floor-staff — The policy or the target does not exist. |
| `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/password/forgot/{email}

**Request a password reset**

`operationId: UsersController_passwordForgot`

Sends a password reset link (or, with `strategy=temporary_password`, a temporary password) to a staff user. When the password policy governing that person has `allowSelfServiceReset: false`, nothing is sent, the refusal is logged, and the response is exactly the one an unknown address gets — the switch cannot be used to learn which addresses have accounts. HR issuing a new hire's temporary password does not go through this route and is not affected.

#### Signature

```http
GET /profile/password/forgot/{email} (email: string) -> The request result
```

#### Access

Public — no credentials required.

#### Notes

- Rate-limit it — otherwise it doubles as an email-bombing tool.

#### Errors

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

#### See also

- `POST /password/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. |
| `email` | path | string | yes | Email address. |
| `strategy` | query | string | yes |  |
| `password` | query | string | yes |  |
| `redirectUrl` | query | string | yes |  |

### Responses

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

## GET /profile/user/password/forgot/{email}

**Request a password reset**

`operationId: UsersController_passwordForgot`

Sends a password reset link (or, with `strategy=temporary_password`, a temporary password) to a staff user. When the password policy governing that person has `allowSelfServiceReset: false`, nothing is sent, the refusal is logged, and the response is exactly the one an unknown address gets — the switch cannot be used to learn which addresses have accounts. HR issuing a new hire's temporary password does not go through this route and is not affected.

#### Signature

```http
GET /profile/user/password/forgot/{email} (email: string) -> The request result
```

#### Access

Public — no credentials required.

#### Notes

- Rate-limit it — otherwise it doubles as an email-bombing tool.

#### Errors

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

#### See also

- `POST /password/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. |
| `email` | path | string | yes | Email address. |
| `strategy` | query | string | yes |  |
| `password` | query | string | yes |  |
| `redirectUrl` | query | string | yes |  |

### Responses

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

## GET /user/password/forgot/{email}

**Request a password reset**

`operationId: UsersController_passwordForgot`

Sends a password reset link (or, with `strategy=temporary_password`, a temporary password) to a staff user. When the password policy governing that person has `allowSelfServiceReset: false`, nothing is sent, the refusal is logged, and the response is exactly the one an unknown address gets — the switch cannot be used to learn which addresses have accounts. HR issuing a new hire's temporary password does not go through this route and is not affected.

#### Signature

```http
GET /user/password/forgot/{email} (email: string) -> The request result
```

#### Access

Public — no credentials required.

#### Notes

- Rate-limit it — otherwise it doubles as an email-bombing tool.

#### Errors

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

#### See also

- `POST /password/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. |
| `email` | path | string | yes | Email address. |
| `strategy` | query | string | yes |  |
| `password` | query | string | yes |  |
| `redirectUrl` | query | string | yes |  |

### Responses

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

## GET /user/user/password/forgot/{email}

**Request a password reset**

`operationId: UsersController_passwordForgot`

Sends a password reset link (or, with `strategy=temporary_password`, a temporary password) to a staff user. When the password policy governing that person has `allowSelfServiceReset: false`, nothing is sent, the refusal is logged, and the response is exactly the one an unknown address gets — the switch cannot be used to learn which addresses have accounts. HR issuing a new hire's temporary password does not go through this route and is not affected.

#### Signature

```http
GET /user/user/password/forgot/{email} (email: string) -> The request result
```

#### Access

Public — no credentials required.

#### Notes

- Rate-limit it — otherwise it doubles as an email-bombing tool.

#### Errors

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

#### See also

- `POST /password/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. |
| `email` | path | string | yes | Email address. |
| `strategy` | query | string | yes |  |
| `password` | query | string | yes |  |
| `redirectUrl` | query | string | yes |  |

### Responses

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

## POST /profile/password/reset

**Reset a password**

`operationId: UsersController_passwordReset`

Sets a new password using a reset token. The token should be single-use and short-lived; validate it first if you want to check before asking for the new password. For a staff user the governing password policy is checked again: with self-service reset switched off the token is refused as invalid (a link issued before the switch no longer works), and the new password must meet the policy's rules.

#### Signature

```http
POST /profile/password/reset (body) -> The result
```

#### Access

Public — no credentials required.

#### Errors

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

#### See also

- `POST /password/validate-token`

### 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. |
| `x-client-info` | header | string | — | Client context recorded against the attempt — used for device tracking and blocking. |

### Request body

Token and new password.

```json
{
  "token": "rst_9k2m4h1p7q",
  "newPassword": "…"
}
```

### Responses

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

## POST /profile/user/password/reset

**Reset a password**

`operationId: UsersController_passwordReset`

Sets a new password using a reset token. The token should be single-use and short-lived; validate it first if you want to check before asking for the new password. For a staff user the governing password policy is checked again: with self-service reset switched off the token is refused as invalid (a link issued before the switch no longer works), and the new password must meet the policy's rules.

#### Signature

```http
POST /profile/user/password/reset (body) -> The result
```

#### Access

Public — no credentials required.

#### Errors

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

#### See also

- `POST /password/validate-token`

### 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. |
| `x-client-info` | header | string | — | Client context recorded against the attempt — used for device tracking and blocking. |

### Request body

Token and new password.

```json
{
  "token": "rst_9k2m4h1p7q",
  "newPassword": "…"
}
```

### Responses

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

## POST /user/password/reset

**Reset a password**

`operationId: UsersController_passwordReset`

Sets a new password using a reset token. The token should be single-use and short-lived; validate it first if you want to check before asking for the new password. For a staff user the governing password policy is checked again: with self-service reset switched off the token is refused as invalid (a link issued before the switch no longer works), and the new password must meet the policy's rules.

#### Signature

```http
POST /user/password/reset (body) -> The result
```

#### Access

Public — no credentials required.

#### Errors

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

#### See also

- `POST /password/validate-token`

### 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. |
| `x-client-info` | header | string | — | Client context recorded against the attempt — used for device tracking and blocking. |

### Request body

Token and new password.

```json
{
  "token": "rst_9k2m4h1p7q",
  "newPassword": "…"
}
```

### Responses

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

## POST /user/user/password/reset

**Reset a password**

`operationId: UsersController_passwordReset`

Sets a new password using a reset token. The token should be single-use and short-lived; validate it first if you want to check before asking for the new password. For a staff user the governing password policy is checked again: with self-service reset switched off the token is refused as invalid (a link issued before the switch no longer works), and the new password must meet the policy's rules.

#### Signature

```http
POST /user/user/password/reset (body) -> The result
```

#### Access

Public — no credentials required.

#### Errors

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

#### See also

- `POST /password/validate-token`

### 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. |
| `x-client-info` | header | string | — | Client context recorded against the attempt — used for device tracking and blocking. |

### Request body

Token and new password.

```json
{
  "token": "rst_9k2m4h1p7q",
  "newPassword": "…"
}
```

### Responses

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

## POST /profile/password/validate-token

**Validate a reset token**

`operationId: UsersController_validateResetToken`

Checks a password reset token before showing the reset form, so an expired link fails early rather than after the user has typed a new password.

#### Signature

```http
POST /profile/password/validate-token (body) -> Whether the token is valid
```

#### Access

Public — no credentials required.

#### Errors

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

#### See also

- `POST /password/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. |

### Request body

The token to validate.

```json
{
  "token": "rst_9k2m4h1p7q"
}
```

### Responses

| Status | Meaning |
| --- | --- |
| `201` | Whether the token is valid |
| `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/password/validate-token

**Validate a reset token**

`operationId: UsersController_validateResetToken`

Checks a password reset token before showing the reset form, so an expired link fails early rather than after the user has typed a new password.

#### Signature

```http
POST /profile/user/password/validate-token (body) -> Whether the token is valid
```

#### Access

Public — no credentials required.

#### Errors

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

#### See also

- `POST /password/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. |

### Request body

The token to validate.

```json
{
  "token": "rst_9k2m4h1p7q"
}
```

### Responses

| Status | Meaning |
| --- | --- |
| `201` | Whether the token is valid |
| `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/password/validate-token

**Validate a reset token**

`operationId: UsersController_validateResetToken`

Checks a password reset token before showing the reset form, so an expired link fails early rather than after the user has typed a new password.

#### Signature

```http
POST /user/password/validate-token (body) -> Whether the token is valid
```

#### Access

Public — no credentials required.

#### Errors

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

#### See also

- `POST /password/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. |

### Request body

The token to validate.

```json
{
  "token": "rst_9k2m4h1p7q"
}
```

### Responses

| Status | Meaning |
| --- | --- |
| `201` | Whether the token is valid |
| `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/password/validate-token

**Validate a reset token**

`operationId: UsersController_validateResetToken`

Checks a password reset token before showing the reset form, so an expired link fails early rather than after the user has typed a new password.

#### Signature

```http
POST /user/user/password/validate-token (body) -> Whether the token is valid
```

#### Access

Public — no credentials required.

#### Errors

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

#### See also

- `POST /password/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. |

### Request body

The token to validate.

```json
{
  "token": "rst_9k2m4h1p7q"
}
```

### Responses

| Status | Meaning |
| --- | --- |
| `201` | Whether the token is valid |
| `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/validate

**Validate an invitation**

`operationId: UsersController_inviteValidate`

Checks an invitation token before showing the acceptance form, so an expired or used invitation fails before the user fills anything in.

#### Signature

```http
POST /profile/user/invite/validate (body) -> Whether the invitation is valid
```

#### Access

Public — no credentials required.

#### Errors

| Status | Code | Message | When | What to do |
| --- | --- | --- | --- | --- |
| `400` | INVALID_TOKEN | Invalid or expired invitation token | The token is unrecognised or has expired. | Ask an admin to resend the invitation. |

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

#### See also

- `POST /user/invite/complete`

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

```json
{
  "token": "inv_9k2m4h1p7q"
}
```

### Responses

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

## POST /user/user/invite/validate

**Validate an invitation**

`operationId: UsersController_inviteValidate`

Checks an invitation token before showing the acceptance form, so an expired or used invitation fails before the user fills anything in.

#### Signature

```http
POST /user/user/invite/validate (body) -> Whether the invitation is valid
```

#### Access

Public — no credentials required.

#### Errors

| Status | Code | Message | When | What to do |
| --- | --- | --- | --- | --- |
| `400` | INVALID_TOKEN | Invalid or expired invitation token | The token is unrecognised or has expired. | Ask an admin to resend the invitation. |

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

#### See also

- `POST /user/invite/complete`

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

```json
{
  "token": "inv_9k2m4h1p7q"
}
```

### Responses

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

## POST /profile/user/invite/complete

**Complete an invitation**

`operationId: UsersController_inviteComplete`

Accepts an invitation and creates the account. An invitation can only be completed once.

#### Signature

```http
POST /profile/user/invite/complete (body) -> The created account and tokens
```

#### Access

Public — no credentials required.

#### Errors

| Status | Code | Message | When | What to do |
| --- | --- | --- | --- | --- |
| `400` | ALREADY_COMPLETED | This invitation has already been completed | The invitation was used. | Sign in instead. |

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

#### See also

- `POST /user/invite/validate`

### 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 token and account details.

```json
{
  "token": "inv_9k2m4h1p7q",
  "password": "…",
  "firstName": "Grace",
  "lastName": "Hopper"
}
```

### Responses

| Status | Meaning |
| --- | --- |
| `201` | The created account and tokens |
| `400` | This invitation has already been completed — The invitation was used. |
| `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/complete

**Complete an invitation**

`operationId: UsersController_inviteComplete`

Accepts an invitation and creates the account. An invitation can only be completed once.

#### Signature

```http
POST /user/user/invite/complete (body) -> The created account and tokens
```

#### Access

Public — no credentials required.

#### Errors

| Status | Code | Message | When | What to do |
| --- | --- | --- | --- | --- |
| `400` | ALREADY_COMPLETED | This invitation has already been completed | The invitation was used. | Sign in instead. |

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

#### See also

- `POST /user/invite/validate`

### 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 token and account details.

```json
{
  "token": "inv_9k2m4h1p7q",
  "password": "…",
  "firstName": "Grace",
  "lastName": "Hopper"
}
```

### Responses

| Status | Meaning |
| --- | --- |
| `201` | The created account and tokens |
| `400` | This invitation has already been completed — The invitation was used. |
| `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/lookup

**Find the orgs an email belongs to**

`operationId: UsersController_directoryLookup`

Sign-in step one when the org is not known: the organizations this email has an account in, most recently used first, as `{ orgId, displayName }`. An unknown email answers `{ orgs: [] }`.

#### Signature

```http
POST /profile/user/directory/lookup (body) -> { orgs }
```

#### Access

Public — no credentials required.

#### Notes

- Unauthenticated, and it reveals which orgs an address belongs to — an enumeration surface.

#### Errors

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

#### See also

- `POST /signin`

### Request body

The email.

```json
{
  "email": "ada@example.com"
}
```

### Responses

| Status | Meaning |
| --- | --- |
| `201` | { orgs } |
| `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": [
    {
      "orgId": "acme",
      "displayName": "Acme"
    }
  ]
}
```

## POST /user/user/directory/lookup

**Find the orgs an email belongs to**

`operationId: UsersController_directoryLookup`

Sign-in step one when the org is not known: the organizations this email has an account in, most recently used first, as `{ orgId, displayName }`. An unknown email answers `{ orgs: [] }`.

#### Signature

```http
POST /user/user/directory/lookup (body) -> { orgs }
```

#### Access

Public — no credentials required.

#### Notes

- Unauthenticated, and it reveals which orgs an address belongs to — an enumeration surface.

#### Errors

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

#### See also

- `POST /signin`

### Request body

The email.

```json
{
  "email": "ada@example.com"
}
```

### Responses

| Status | Meaning |
| --- | --- |
| `201` | { orgs } |
| `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": [
    {
      "orgId": "acme",
      "displayName": "Acme"
    }
  ]
}
```

## POST /profile/session-access

**Get a session access token**

`operationId: UsersController_sessionAccessToken`

Issues a scoped access token for the current session — used where a component needs a narrower token than the caller holds.

#### Signature

```http
POST /profile/session-access (body) -> The scoped token
```

#### Access

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

#### Errors

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

#### See also

- `POST /user/refresh`

### 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 the token is for.

```json
{
  "scope": "storefront"
}
```

### Responses

| Status | Meaning |
| --- | --- |
| `201` | The scoped token |
| `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/session-access

**Get a session access token**

`operationId: UsersController_sessionAccessToken`

Issues a scoped access token for the current session — used where a component needs a narrower token than the caller holds.

#### Signature

```http
POST /user/session-access (body) -> The scoped token
```

#### Access

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

#### Errors

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

#### See also

- `POST /user/refresh`

### 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 the token is for.

```json
{
  "scope": "storefront"
}
```

### Responses

| Status | Meaning |
| --- | --- |
| `201` | The scoped token |
| `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/validate-app-key

**Validate an application key**

`operationId: UsersController_validateAppKey`

Checks whether an application key is valid and active — what a client calls to confirm its credentials before relying on them.

#### Signature

```http
POST /profile/validate-app-key (body) -> Whether the key is valid
```

#### Access

Public — no credentials required.

#### Errors

Plus the standard platform errors: `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. |

### Request body

The key to validate.

```json
{
  "appId": "storefront-web",
  "key": "ak_9k2m4h1p7q"
}
```

### Responses

| Status | Meaning |
| --- | --- |
| `201` | Whether the key is valid |
| `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/validate-app-key

**Validate an application key**

`operationId: UsersController_validateAppKey`

Checks whether an application key is valid and active — what a client calls to confirm its credentials before relying on them.

#### Signature

```http
POST /user/validate-app-key (body) -> Whether the key is valid
```

#### Access

Public — no credentials required.

#### Errors

Plus the standard platform errors: `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. |

### Request body

The key to validate.

```json
{
  "appId": "storefront-web",
  "key": "ak_9k2m4h1p7q"
}
```

### Responses

| Status | Meaning |
| --- | --- |
| `201` | Whether the key is valid |
| `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/shared-site-auth/{token}

**Authenticate via a shared-site token**

`operationId: UsersController_userSharedSiteAuth`

Signs a user in from a token issued by another site in the same estate — the cross-site single sign-on hop. The token is a bearer credential in a URL, so it should be short-lived and single-use.

#### Signature

```http
GET /profile/user/shared-site-auth/{token} (token: string) -> Tokens and the signed-in user
```

#### Access

Public — no credentials required.

#### Notes

- Credentials in URLs leak through logs and referrers — keep the lifetime short.

#### Errors

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

#### See also

- `POST /signin`

### 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. |
| `token` | path | string | yes | Shared-site auth token. |

### Responses

| Status | Meaning |
| --- | --- |
| `200` | Tokens and the signed-in user |
| `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/shared-site-auth/{token}

**Authenticate via a shared-site token**

`operationId: UsersController_userSharedSiteAuth`

Signs a user in from a token issued by another site in the same estate — the cross-site single sign-on hop. The token is a bearer credential in a URL, so it should be short-lived and single-use.

#### Signature

```http
GET /user/user/shared-site-auth/{token} (token: string) -> Tokens and the signed-in user
```

#### Access

Public — no credentials required.

#### Notes

- Credentials in URLs leak through logs and referrers — keep the lifetime short.

#### Errors

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

#### See also

- `POST /signin`

### 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. |
| `token` | path | string | yes | Shared-site auth token. |

### Responses

| Status | Meaning |
| --- | --- |
| `200` | Tokens and the signed-in user |
| `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/global-login

**Global login**

`operationId: UsersController_globalLogin`

Authenticates across organizations rather than within one — for a user who belongs to several and has not yet chosen.

#### Signature

```http
POST /profile/global-login (body) -> Tokens and the organizations available
```

#### Access

Public — no credentials required.

#### Errors

| Status | Code | Message | When | What to do |
| --- | --- | --- | --- | --- |
| `400` | INVALID_CREDENTIALS | Invalid username or password | The identifier or password is wrong. | Deliberately does not distinguish an unknown account from a wrong password — do not surface a more specific message to the caller. |

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

#### See also

- `GET /system-orgs`

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

Credentials.

```json
{
  "email": "ada@example.com",
  "password": "…"
}
```

### Responses

| Status | Meaning |
| --- | --- |
| `201` | Tokens and the organizations available |
| `400` | Invalid username or password — The identifier or password is wrong. |
| `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/global-login

**Global login**

`operationId: UsersController_globalLogin`

Authenticates across organizations rather than within one — for a user who belongs to several and has not yet chosen.

#### Signature

```http
POST /user/global-login (body) -> Tokens and the organizations available
```

#### Access

Public — no credentials required.

#### Errors

| Status | Code | Message | When | What to do |
| --- | --- | --- | --- | --- |
| `400` | INVALID_CREDENTIALS | Invalid username or password | The identifier or password is wrong. | Deliberately does not distinguish an unknown account from a wrong password — do not surface a more specific message to the caller. |

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

#### See also

- `GET /system-orgs`

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

Credentials.

```json
{
  "email": "ada@example.com",
  "password": "…"
}
```

### Responses

| Status | Meaning |
| --- | --- |
| `201` | Tokens and the organizations available |
| `400` | Invalid username or password — The identifier or password is wrong. |
| `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/code/{email}

**Request a login code**

`operationId: UsersController_codeLogin`

Sends a one-time login code to an email address — passwordless sign-in.

#### Signature

```http
GET /profile/code/{email} (email: string) -> The request result
```

#### Access

Public — no credentials required.

#### Notes

- Rate-limit per address and per IP.

#### Errors

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

#### See also

- `GET /magic-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. |
| `email` | path | string | yes | Email address. |

### Responses

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

## GET /user/code/{email}

**Request a login code**

`operationId: UsersController_codeLogin`

Sends a one-time login code to an email address — passwordless sign-in.

#### Signature

```http
GET /user/code/{email} (email: string) -> The request result
```

#### Access

Public — no credentials required.

#### Notes

- Rate-limit per address and per IP.

#### Errors

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

#### See also

- `GET /magic-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. |
| `email` | path | string | yes | Email address. |

### Responses

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

## GET /profile/magic-link

**Sign in with a magic link**

`operationId: UsersController_magicLinkLogin`

Completes a passwordless sign-in from an emailed link. The link is a credential — anyone with it is signed in, so keep it short-lived.

#### Signature

```http
GET /profile/magic-link (token?: string) -> Tokens and the signed-in user
```

#### Access

Public — no credentials required.

#### Errors

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

#### See also

- `POST /magic-link/redirect`

### 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. |
| `x-client-host` | header | string | yes |  |
| `x-client-info` | header | string | yes |  |
| `email` | query | string | yes |  |
| `type` | query | string | yes |  |
| `deliveryType` | query | string | yes |  |
| `phone` | query | string | yes |  |
| `token` | query | string | yes | Magic link token. |

### Responses

| Status | Meaning |
| --- | --- |
| `200` | Tokens and the signed-in user |
| `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/magic-link

**Sign in with a magic link**

`operationId: UsersController_magicLinkLogin`

Completes a passwordless sign-in from an emailed link. The link is a credential — anyone with it is signed in, so keep it short-lived.

#### Signature

```http
GET /user/magic-link (token?: string) -> Tokens and the signed-in user
```

#### Access

Public — no credentials required.

#### Errors

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

#### See also

- `POST /magic-link/redirect`

### 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. |
| `x-client-host` | header | string | yes |  |
| `x-client-info` | header | string | yes |  |
| `email` | query | string | yes |  |
| `type` | query | string | yes |  |
| `deliveryType` | query | string | yes |  |
| `phone` | query | string | yes |  |
| `token` | query | string | yes | Magic link token. |

### Responses

| Status | Meaning |
| --- | --- |
| `200` | Tokens and the signed-in user |
| `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/magic-link/redirect

**Redirect after a magic link**

`operationId: UsersController_magicLinkLoginRedirect`

Completes the magic-link flow and redirects the browser onward.

#### Signature

```http
POST /profile/magic-link/redirect (body) -> The redirect result
```

#### Access

Public — no credentials required.

#### Errors

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

#### See also

- `GET /magic-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. |
| `x-client-host` | header | string | yes |  |

### Request body

The token and destination.

```json
{
  "token": "mlk_9k2m4h1p7q",
  "redirectUrl": "https://app.example.com/"
}
```

### Responses

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

## POST /user/magic-link/redirect

**Redirect after a magic link**

`operationId: UsersController_magicLinkLoginRedirect`

Completes the magic-link flow and redirects the browser onward.

#### Signature

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

#### Access

Public — no credentials required.

#### Errors

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

#### See also

- `GET /magic-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. |
| `x-client-host` | header | string | yes |  |

### Request body

The token and destination.

```json
{
  "token": "mlk_9k2m4h1p7q",
  "redirectUrl": "https://app.example.com/"
}
```

### Responses

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

## GET /profile/user/magic-link

**Sign in with a magic link (user)**

`operationId: UsersController_magicLinkUserLogin`

The user-scoped magic-link sign-in, distinct from the customer flow.

#### Signature

```http
GET /profile/user/magic-link (token?: string) -> Tokens and the signed-in user
```

#### Access

Public — no credentials required.

#### Errors

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

#### See also

- `POST /user/magic-link/redirect`

### 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. |
| `email` | query | string | yes |  |
| `type` | query | string | yes |  |
| `token` | query | string | yes |  |

### Responses

| Status | Meaning |
| --- | --- |
| `200` | Tokens and the signed-in user |
| `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/magic-link

**Sign in with a magic link (user)**

`operationId: UsersController_magicLinkUserLogin`

The user-scoped magic-link sign-in, distinct from the customer flow.

#### Signature

```http
GET /user/user/magic-link (token?: string) -> Tokens and the signed-in user
```

#### Access

Public — no credentials required.

#### Errors

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

#### See also

- `POST /user/magic-link/redirect`

### 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. |
| `email` | query | string | yes |  |
| `type` | query | string | yes |  |
| `token` | query | string | yes |  |

### Responses

| Status | Meaning |
| --- | --- |
| `200` | Tokens and the signed-in user |
| `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/magic-link/redirect

**Redirect after a user magic link**

`operationId: UsersController_magicLinkUserRedirect`

Completes the user magic-link flow and redirects onward.

#### Signature

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

#### Access

Public — no credentials required.

#### Errors

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

#### See also

- `GET /user/magic-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. |

### Request body

The token and destination.

```json
{
  "token": "mlk_9k2m4h1p7q"
}
```

### Responses

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

## POST /user/user/magic-link/redirect

**Redirect after a user magic link**

`operationId: UsersController_magicLinkUserRedirect`

Completes the user magic-link flow and redirects onward.

#### Signature

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

#### Access

Public — no credentials required.

#### Errors

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

#### See also

- `GET /user/magic-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. |

### Request body

The token and destination.

```json
{
  "token": "mlk_9k2m4h1p7q"
}
```

### Responses

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

## GET /profile/facebook/url

**Get the Facebook auth URL**

`operationId: UsersController_facebookAuthUrl`

Returns the URL to send a browser to for Facebook sign-in, for clients that redirect themselves.

#### Signature

```http
GET /profile/facebook/url () -> The auth URL
```

#### Access

Public — no credentials required.

#### Errors

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

#### See also

- `GET /facebook`

### Parameters

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

### Responses

| Status | Meaning |
| --- | --- |
| `200` | The auth URL |
| `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/facebook/url

**Get the Facebook auth URL**

`operationId: UsersController_facebookAuthUrl`

Returns the URL to send a browser to for Facebook sign-in, for clients that redirect themselves.

#### Signature

```http
GET /user/facebook/url () -> The auth URL
```

#### Access

Public — no credentials required.

#### Errors

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

#### See also

- `GET /facebook`

### Parameters

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

### Responses

| Status | Meaning |
| --- | --- |
| `200` | The auth URL |
| `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/facebook

**Start Facebook sign-in**

`operationId: UsersController_facebookLogin`

Begins the Facebook OAuth flow.

#### Signature

```http
GET /profile/facebook () -> Redirect to Facebook
```

#### Access

Public — no credentials required.

#### Errors

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

#### See also

- `GET /facebook/redirect`

### 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` | Redirect to Facebook |
| `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/facebook

**Start Facebook sign-in**

`operationId: UsersController_facebookLogin`

Begins the Facebook OAuth flow.

#### Signature

```http
GET /user/facebook () -> Redirect to Facebook
```

#### Access

Public — no credentials required.

#### Errors

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

#### See also

- `GET /facebook/redirect`

### 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` | Redirect to Facebook |
| `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/facebook/redirect

**Facebook sign-in callback**

`operationId: UsersController_facebookLoginCallback`

Handles Facebook's OAuth callback and issues tokens.

#### Signature

```http
GET /profile/facebook/redirect () -> Tokens and the signed-in user
```

#### Access

Public — no credentials required.

#### Errors

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

#### See also

- `POST /facebook/token`

### 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` | Tokens and the signed-in user |
| `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/facebook/redirect

**Facebook sign-in callback**

`operationId: UsersController_facebookLoginCallback`

Handles Facebook's OAuth callback and issues tokens.

#### Signature

```http
GET /user/facebook/redirect () -> Tokens and the signed-in user
```

#### Access

Public — no credentials required.

#### Errors

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

#### See also

- `POST /facebook/token`

### 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` | Tokens and the signed-in user |
| `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/facebook/token

**Exchange a Facebook token**

`operationId: UsersController_facebookTokenExchange`

Exchanges a Facebook access token for platform tokens — the native-app flow, where the SDK obtains the token rather than a browser redirect.

#### Signature

```http
POST /profile/facebook/token (body) -> Tokens and the signed-in user
```

#### Access

Public — no credentials required.

#### Notes

- Verify the token with the provider — a client-supplied token is not evidence on its own.

#### Errors

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

#### See also

- `GET /facebook/redirect`

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

```json
{
  "accessToken": "EAAG…"
}
```

### Responses

| Status | Meaning |
| --- | --- |
| `201` | Tokens and the signed-in user |
| `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/facebook/token

**Exchange a Facebook token**

`operationId: UsersController_facebookTokenExchange`

Exchanges a Facebook access token for platform tokens — the native-app flow, where the SDK obtains the token rather than a browser redirect.

#### Signature

```http
POST /user/facebook/token (body) -> Tokens and the signed-in user
```

#### Access

Public — no credentials required.

#### Notes

- Verify the token with the provider — a client-supplied token is not evidence on its own.

#### Errors

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

#### See also

- `GET /facebook/redirect`

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

```json
{
  "accessToken": "EAAG…"
}
```

### Responses

| Status | Meaning |
| --- | --- |
| `201` | Tokens and the signed-in user |
| `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/google

**Start Google sign-in**

`operationId: UsersController_googleLogin`

Begins the Google OAuth flow.

#### Signature

```http
GET /profile/google () -> Redirect to Google
```

#### Access

Public — no credentials required.

#### Errors

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

#### See also

- `GET /google/redirect`

### 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` | Redirect to Google |
| `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/google

**Start Google sign-in**

`operationId: UsersController_googleLogin`

Begins the Google OAuth flow.

#### Signature

```http
GET /user/google () -> Redirect to Google
```

#### Access

Public — no credentials required.

#### Errors

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

#### See also

- `GET /google/redirect`

### 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` | Redirect to Google |
| `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/google/redirect

**Google sign-in callback**

`operationId: UsersController_googleLoginCallback`

Handles Google's OAuth callback and issues tokens.

#### Signature

```http
GET /profile/google/redirect () -> Tokens and the signed-in user
```

#### Access

Public — no credentials required.

#### Errors

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

#### See also

- `POST /customer/google/token`

### 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` | Tokens and the signed-in user |
| `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/google/redirect

**Google sign-in callback**

`operationId: UsersController_googleLoginCallback`

Handles Google's OAuth callback and issues tokens.

#### Signature

```http
GET /user/google/redirect () -> Tokens and the signed-in user
```

#### Access

Public — no credentials required.

#### Errors

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

#### See also

- `POST /customer/google/token`

### 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` | Tokens and the signed-in user |
| `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/github/url

**Get the GitHub auth URL**

`operationId: UsersController_githubAuthUrl`

Returns the URL to send a browser to for GitHub sign-in.

#### Signature

```http
GET /profile/github/url () -> The auth URL
```

#### Access

Public — no credentials required.

#### Errors

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

#### See also

- `GET /github`

### Parameters

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

### Responses

| Status | Meaning |
| --- | --- |
| `200` | The auth URL |
| `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/github/url

**Get the GitHub auth URL**

`operationId: UsersController_githubAuthUrl`

Returns the URL to send a browser to for GitHub sign-in.

#### Signature

```http
GET /user/github/url () -> The auth URL
```

#### Access

Public — no credentials required.

#### Errors

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

#### See also

- `GET /github`

### Parameters

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

### Responses

| Status | Meaning |
| --- | --- |
| `200` | The auth URL |
| `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/github

**Start GitHub sign-in**

`operationId: UsersController_githubLogin`

Begins the GitHub OAuth flow.

#### Signature

```http
GET /profile/github () -> Redirect to GitHub
```

#### Access

Public — no credentials required.

#### Errors

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

#### See also

- `GET /github/redirect`

### 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` | Redirect to GitHub |
| `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/github

**Start GitHub sign-in**

`operationId: UsersController_githubLogin`

Begins the GitHub OAuth flow.

#### Signature

```http
GET /user/github () -> Redirect to GitHub
```

#### Access

Public — no credentials required.

#### Errors

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

#### See also

- `GET /github/redirect`

### 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` | Redirect to GitHub |
| `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/github/redirect

**GitHub sign-in callback**

`operationId: UsersController_githubLoginCallback`

Handles GitHub's OAuth callback and issues tokens.

#### Signature

```http
GET /profile/github/redirect () -> Tokens and the signed-in user
```

#### Access

Public — no credentials required.

#### Errors

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

#### See also

- `POST /github/redirect`

### 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` | Tokens and the signed-in user |
| `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/github/redirect

**GitHub sign-in callback (POST)**

`operationId: UsersController_githubLoginCallbackPost`

The POST form of the GitHub callback, for clients that post the authorisation code rather than being redirected.

#### Signature

```http
POST /profile/github/redirect (body) -> Tokens and the signed-in user
```

#### Access

Public — no credentials required.

#### Errors

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

#### See also

- `GET /github/redirect`

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

```json
{
  "code": "abc123"
}
```

### Responses

| Status | Meaning |
| --- | --- |
| `201` | Tokens and the signed-in user |
| `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/github/redirect

**GitHub sign-in callback**

`operationId: UsersController_githubLoginCallback`

Handles GitHub's OAuth callback and issues tokens.

#### Signature

```http
GET /user/github/redirect () -> Tokens and the signed-in user
```

#### Access

Public — no credentials required.

#### Errors

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

#### See also

- `POST /github/redirect`

### 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` | Tokens and the signed-in user |
| `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/github/redirect

**GitHub sign-in callback (POST)**

`operationId: UsersController_githubLoginCallbackPost`

The POST form of the GitHub callback, for clients that post the authorisation code rather than being redirected.

#### Signature

```http
POST /user/github/redirect (body) -> Tokens and the signed-in user
```

#### Access

Public — no credentials required.

#### Errors

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

#### See also

- `GET /github/redirect`

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

```json
{
  "code": "abc123"
}
```

### Responses

| Status | Meaning |
| --- | --- |
| `201` | Tokens and the signed-in user |
| `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/github/token

**Exchange a GitHub token**

`operationId: UsersController_githubTokenExchange`

Exchanges a GitHub access token for platform tokens.

#### Signature

```http
POST /profile/github/token (body) -> Tokens and the signed-in user
```

#### Access

Public — no credentials required.

#### Errors

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

#### See also

- `POST /facebook/token`

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

```json
{
  "accessToken": "gho_…"
}
```

### Responses

| Status | Meaning |
| --- | --- |
| `201` | Tokens and the signed-in user |
| `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/github/token

**Exchange a GitHub token**

`operationId: UsersController_githubTokenExchange`

Exchanges a GitHub access token for platform tokens.

#### Signature

```http
POST /user/github/token (body) -> Tokens and the signed-in user
```

#### Access

Public — no credentials required.

#### Errors

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

#### See also

- `POST /facebook/token`

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

```json
{
  "accessToken": "gho_…"
}
```

### Responses

| Status | Meaning |
| --- | --- |
| `201` | Tokens and the signed-in user |
| `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/auth/success

**Authentication success callback**

`operationId: UsersController_authSuccess`

The landing endpoint after a successful external authentication flow.

#### Signature

```http
GET /profile/auth/success () -> Authentication result
```

#### Access

Public — no credentials required.

#### Errors

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

#### See also

- `GET /google/redirect`

### Parameters

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

## GET /user/auth/success

**Authentication success callback**

`operationId: UsersController_authSuccess`

The landing endpoint after a successful external authentication flow.

#### Signature

```http
GET /user/auth/success () -> Authentication result
```

#### Access

Public — no credentials required.

#### Errors

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

#### See also

- `GET /google/redirect`

### Parameters

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

