# Users and invitations — tool reference

Generated from the OpenAPI document (`https://maison-labs.com/api/v1/openapi.json`). Field types are JSON Schema types; GET/DELETE inputs travel as path or query, other methods as a JSON body.

## `add_client_user` — Add a user to a hotel

Give a person access to a hotel with roles and groups, in one call, whether or not they have a Maison account. Invites are per hotel: an existing user not yet at this hotel gets pending links and an invite email; one who already has accepted access here gets the roles and groups added directly, live at once, with no invite and no email (`kind: 'attached'`, `emailSent: null`); an email new to Maison gets an account (optional firstName / lastName, at most 25 characters each), the links and the invite email. `mode` 'create' (create only) makes a new account active at once with the password in `auth` (base64, at least 8 characters decoded), every link live, no email of any kind (409 when the email already has an account — give access instead); mode 'invite' refuses `auth`. On an invite, `destination` picks where the accept link lands: the Admin Dashboard (`{ kind: 'admin' }`) or this hotel's demo site with the QA editor open (`{ kind: 'demo', preview_website_id }`, a `demoSites[].id` from `get_client_overview`; 404 when it is not this hotel's). Ask the operator both choices (create only or invite; Admin Dashboard or demo site with the QA editor) before calling when they were not stated. Returns `{ kind: 'invited' | 'attached', userId, created, emailSent, invitationFor }` — `invitationFor` names the product the invite email opens (`'Admin Dashboard'` or `'Demo site (QA editor)'`; null when no invite was sent). A person whose roles administer the hotel is added to its chat log analytics subscribers. Every role and group must be assignable here (403 names the refused one). 409 when the person is disabled, already has every chosen role and group here, reaches every hotel through a Maison-wide role, or is only invited here so far (use `resend_client_invite`). 502 when the invite was written but the email could not be sent (call `resend_client_invite`). Use `invite_user` for roles at several hotels or Maison-wide. For an existing user, 409 `invite_account_mismatch` when the person's sign-in account does not match their Maison user (a Maison support fix; no email is sent); 502 `invite_account_unavailable` when the sign-in account cannot be checked (no email is sent; retry).

- REST: `POST https://maison-labs.com/api/v1/client/{clientId}/user`
- MCP tool / A2A skill: `add_client_user`
- Needs: maison-users WRITE (client)

Inputs:

| Field | Where | Type | Required | Notes |
| --- | --- | --- | --- | --- |
| `clientId` | path | string | yes | Client (hotel) id; format uuid |
| `email` | body | string | yes | The person's email address; format email |
| `roleIds` | body | array of string | no | Role ids to give at this hotel; default [] |
| `groupIds` | body | array of string | no | This hotel's group ids to add them to; default [] |
| `firstName` | body | string | no | Only for a new account; at most 25 characters |
| `lastName` | body | string | no | Only for a new account; at most 25 characters |
| `mode` | body | enum: invite \| create | no | invite: email an accept link. create: create only — account active at once with the password in `auth`, no email sent. API callers default to invite; the Maison agent must send it (without it the tool returns `status: "needs_input"` with the question to ask). Ask the operator before calling when they have not said it: create only, or send an invitation; and if inviting, should the link open the Admin Dashboard or the demo site with the QA editor open. Never pick either silently. |
| `auth` | body | string | no | mode create only (required there): the account's password, base64-encoded UTF-8, at least 8 characters decoded |
| `destination` | body | object | no | mode invite only: API callers default to `{ kind: 'admin' }`; the Maison agent must send it on an invite. Where the accept link lands — `{ kind: 'admin' }` the Admin Dashboard, or `{ kind: 'demo', preview_website_id }` the client's demo site with the QA editor open (the id is a `demoSites[].id` from `get_client_overview`). 404 when that demo site is not this client's. |

## `disable_user` — Disable a user

Soft-disable a user account (sets disabled_date; the sign-in account is NOT deleted) and stop their API keys at once. The user cannot log in until `enable_user`. 404 when the user is unknown; 409 when already disabled.

- REST: `POST https://maison-labs.com/api/v1/user/{userId}/disable`
- MCP tool / A2A skill: `disable_user`
- Needs: maison-users WRITE (global)
- Destructive — confirm with the user first.

Inputs:

| Field | Where | Type | Required | Notes |
| --- | --- | --- | --- | --- |
| `userId` | path | string | yes | User id; format uuid |

## `disable_user_api_key` — Disable a user's API key

Disable one of a user's API keys; calls with it are refused at once. Needs the global API-keys grant (disabling your own key needs none: `disable_my_api_key`). Find the key id with `list_user_api_keys`. 404 when the user has no active key with that id. Keys are never re-enabled: the user creates a new one from their profile.

- REST: `DELETE https://maison-labs.com/api/v1/user/{userId}/apikey/{keyId}`
- MCP tool / A2A skill: `disable_user_api_key`
- Needs: api-keys WRITE (global)
- Destructive — confirm with the user first.

Inputs:

| Field | Where | Type | Required | Notes |
| --- | --- | --- | --- | --- |
| `userId` | path | string | yes | User id; format uuid |
| `keyId` | path | string | yes | API key id (from list_user_api_keys); format uuid |

## `enable_user` — Enable a user

Re-enable a disabled user account so they can sign in again (clears disabled_date). Returns the user's full profile. Re-enabling a holder of a Maison-only role also needs the global roles grant. 404 when the user is unknown. Use `disable_user` to switch an account off.

- REST: `POST https://maison-labs.com/api/v1/user/{userId}/enable`
- MCP tool / A2A skill: `enable_user`
- Needs: maison-users WRITE (global)

Inputs:

| Field | Where | Type | Required | Notes |
| --- | --- | --- | --- | --- |
| `userId` | path | string | yes | User id; format uuid |

## `force_password_reset` — Force a password reset

Flag a user's account so they must set a new password, and email them a link to the reset-code page. 404 when the user is unknown; 409 when the user is disabled or has not accepted their invite.

- REST: `POST https://maison-labs.com/api/v1/user/{userId}/forcereset`
- MCP tool / A2A skill: `force_password_reset`
- Needs: maison-users WRITE (global)

Inputs:

| Field | Where | Type | Required | Notes |
| --- | --- | --- | --- | --- |
| `userId` | path | string | yes | User id; format uuid |

## `get_client_user_access` — Get a user's effective access at a hotel

The full set of feature permissions a user holds at a specific hotel — direct roles and group-inherited grants combined. 404 when the user is not found in the hotel roster.

- REST: `GET https://maison-labs.com/api/v1/client/{clientId}/user/{userId}/access`
- MCP tool / A2A skill: `get_client_user_access`
- Needs: maison-users READ (client)
- Read-only.

Inputs:

| Field | Where | Type | Required | Notes |
| --- | --- | --- | --- | --- |
| `clientId` | path | string | yes | Client (hotel) id; format uuid |
| `userId` | path | string | yes | User id; format uuid |

## `get_user` — Get a user

Fetch a user's full profile: identity fields, computed status, direct and inherited roles, groups, client access, and masked API-key list. 404 when the id is unknown or the user is inaccessible.

- REST: `GET https://maison-labs.com/api/v1/user/{userId}`
- MCP tool / A2A skill: `get_user`
- Needs: maison-users READ (global)
- Read-only.

Inputs:

| Field | Where | Type | Required | Notes |
| --- | --- | --- | --- | --- |
| `userId` | path | string | yes | User id; format uuid |

## `get_user_activity` — User activity

One user's full activity trail for a date range, newest first, each row in plain words (`summary`) with a closed `kind`, its `source`, the hotel and a console link (`href`). Four sources merged: `credentials` (invites sent and resent, passwords set or reset, API keys created or disabled — masked, never the key — connected apps revoked, the account created, enabled or disabled; done to the user or by them, with who, how and the request id), `sessions` (sign-in, sign-out, every session ended by a password reset), `changes` (everything the user created, updated, deleted or disabled, from every table's audit columns) and `activity` (hotel activity events they performed). `from` and `to` are required (ISO 8601, at most 366 days apart); `source` keeps one source; page with `nextCursor`. For every user's credential events at once use `list_credential_audit`.

- REST: `GET https://maison-labs.com/api/v1/user/{userId}/activity`
- MCP tool / A2A skill: `get_user_activity`
- Needs: maison-users READ (global)
- Read-only.

Inputs:

| Field | Where | Type | Required | Notes |
| --- | --- | --- | --- | --- |
| `userId` | path | string | yes | User id; format uuid |
| `from` | query | string | yes | Start of the window, inclusive (ISO 8601); format date-time |
| `to` | query | string | yes | End of the window, exclusive (ISO 8601); at most 366 days after `from`; format date-time |
| `source` | query | enum: credentials \| sessions \| changes \| activity | no | `credentials`, `sessions`, `changes` or `activity`; omit to read all four |
| `limit` | query | integer | no | Page size (1–100); default 50 |
| `cursor` | query | string | no | `nextCursor` of the previous page |

## `invite_user` — Invite a Maison user

Create a new Maison account and send an invite email, giving it roles (each at a client, or at all clients with clientId null) and groups. firstName / lastName are optional, at most 25 characters each. Needs the users grant where each role or group is given — globally for all clients; a caller without the global grant must give at least one role or group. 201 on success. With mode "create" (create only) the account is active at once with the password you give in `auth` (base64 of the password, at least 8 characters once decoded) and no email of any kind is sent — share the password with the person yourself; mode "invite" emails an accept link and refuses `auth`. On an invite, `destination` picks where the link lands: the Admin Dashboard (`{ kind: "admin" }`) or the demo site of the client with the QA editor open (`{ kind: "demo", preview_website_id }`, only for roles at exactly one client and no groups; 400 otherwise). The `invitationFor` field names the product the invite opens (`Admin Dashboard` or `Demo site (QA editor)`; null in create mode). Ask the operator both choices (create only or invite; Admin Dashboard or demo site with the QA editor) before calling when they were not stated. A person whose roles administer a client is added to the chat log analytics subscribers of that client. Use `add_client_user` to give an existing user access to a specific hotel.

- REST: `POST https://maison-labs.com/api/v1/user/invite`
- MCP tool / A2A skill: `invite_user`
- Needs: maison-users WRITE (any)

Inputs:

| Field | Where | Type | Required | Notes |
| --- | --- | --- | --- | --- |
| `email` | body | string | yes | Email address for the new account; format email |
| `firstName` | body | string \| null | no | At most 25 characters; default null |
| `lastName` | body | string \| null | no | At most 25 characters; default null |
| `roles` | body | array of object | no | default [] |
| `groupIds` | body | array of string | no | default [] |
| `inviteExpiryDays` | body | integer | no | Days before the invite link expires; default 7 |
| `mode` | body | enum: invite \| create | no | invite: email an accept link. create: create only — account active at once with the password in `auth`, no email sent. API callers default to invite; the Maison agent must send it (without it the tool returns `status: "needs_input"` with the question to ask). Ask the operator before calling when they have not said it: create only, or send an invitation; and if inviting, should the link open the Admin Dashboard or the demo site with the QA editor open. Never pick either silently. |
| `auth` | body | string | no | mode create only (required there): the account's password, base64-encoded UTF-8, at least 8 characters decoded |
| `destination` | body | object | no | mode invite only: API callers default to `{ kind: 'admin' }`; the Maison agent must send it on an invite. Where the accept link lands — `{ kind: 'admin' }` the Admin Dashboard, or `{ kind: 'demo', preview_website_id }` the client's demo site with the QA editor open (the id is a `demoSites[].id` from `get_client_overview`). 404 when that demo site is not this client's. |

## `list_client_users` — List a hotel's users

All users with access to a specific hotel, both direct role-links and group-inherited memberships. Use `get_client_user_access` for one user's detailed grant breakdown.

- REST: `GET https://maison-labs.com/api/v1/client/{clientId}/users`
- MCP tool / A2A skill: `list_client_users`
- Needs: maison-users READ (client)
- Read-only.

Inputs:

| Field | Where | Type | Required | Notes |
| --- | --- | --- | --- | --- |
| `clientId` | path | string | yes | Client (hotel) id; format uuid |

## `list_credential_audit` — Credential and account audit

Maison staff only. Every credential, account and access event across all users, newest first, with exact timestamps: who set or reset whose password, who created or disabled whose API key (masked, never the key), connected apps revoked, users invited, created, enabled or disabled — each with the acting user, how they authenticated (`session`, `api_key`, `oauth_token`) and the source — and, in the same time order, every user's access changes read from the role and group links (category `access`: `access_granted`, `access_invited`, `invite_accepted`, `access_removed`; one row per change with `detail.client_id`, `detail.roles` and `detail.groups` by name; Maison-only roles hidden without `maison-roles` READ) — plus `summary` (the event in the activity trail's words; a re-invite names the previous invite), `client_name` and, for a re-invite, the refreshed `links` by name and client. Filter by event `type`, `actorUserId`, `targetUserId` and a `from`/`to` window; page with `nextCursor`. For one user's whole activity use `get_user_activity`.

- REST: `GET https://maison-labs.com/api/v1/audit/credentials`
- MCP tool / A2A skill: `list_credential_audit`
- Needs: maison-users READ (global)
- Read-only.

Inputs:

| Field | Where | Type | Required | Notes |
| --- | --- | --- | --- | --- |
| `type` | query | enum: password_set \| password_changed_self \| password_reset_forced \| api_key_created \| api_key_disabled \| connected_app_revoked \| user_invited \| user_reinvited \| user_created \| user_enabled \| user_disabled \| client_deleted \| access_granted \| access_invited \| invite_accepted \| access_removed | no | One event type |
| `category` | query | enum: credential \| account \| access | no | Every event of a category: credential, account or access |
| `actorUserId` | query | string | no | Who did it; format uuid |
| `targetUserId` | query | string | no | Whose account it was done to; format uuid |
| `from` | query | string | no | Earliest time, inclusive (ISO 8601); format date-time |
| `to` | query | string | no | Latest time, exclusive (ISO 8601); format date-time |
| `limit` | query | integer | no | Page size (1–100); default 50 |
| `cursor` | query | string | no | `nextCursor` of the previous page |

## `list_user_api_keys` — List a user's API keys

Returns the masked API-key records for one user (prefix + last-4 characters only — never the raw key). Use to audit access or find a key for `disable_user_api_key`; your own keys need no grant through `list_my_api_keys`.

- REST: `GET https://maison-labs.com/api/v1/user/{userId}/apikeys`
- MCP tool / A2A skill: `list_user_api_keys`
- Needs: maison-users READ (global)
- Read-only.

Inputs:

| Field | Where | Type | Required | Notes |
| --- | --- | --- | --- | --- |
| `userId` | path | string | yes | User id; format uuid |

## `list_user_clients` — List a user's hotels

Page through the hotels a user can act on through their direct roles, one row per (hotel, role), ordered by hotel name. Returns `items`, `total`, `nextCursor` and `hasMore`. Use `get_client_user_access` for what the user may do at one hotel.

- REST: `GET https://maison-labs.com/api/v1/user/{userId}/clients`
- MCP tool / A2A skill: `list_user_clients`
- Needs: maison-users READ (global)
- Read-only.

Inputs:

| Field | Where | Type | Required | Notes |
| --- | --- | --- | --- | --- |
| `userId` | path | string | yes | User id; format uuid |
| `limit` | query | integer | no | Page size (1–100); default 20 |
| `cursor` | query | string | no | `nextCursor` of the previous page |

## `list_user_connected_apps` — List a user's connected apps

The apps a user has connected to their Maison account with OAuth (their active consents): each with the app, the scopes it was granted (grouped by area), the hotels it may act on (`all` or a list), when it was granted and last used. Use `revoke_user_connected_app` to cut one off; `list_my_connected_apps` for your own.

- REST: `GET https://maison-labs.com/api/v1/user/{userId}/connectedapps`
- MCP tool / A2A skill: `list_user_connected_apps`
- Needs: maison-users READ (global)
- Read-only.

Inputs:

| Field | Where | Type | Required | Notes |
| --- | --- | --- | --- | --- |
| `userId` | path | string | yes | User id; format uuid |

## `list_users` — List users

Page through every Maison user with optional search, status filter, and sort. Returns masked API-key counts but never raw keys. Use `get_user` for the full profile of one user.

- REST: `GET https://maison-labs.com/api/v1/users`
- MCP tool / A2A skill: `list_users`
- Needs: maison-users READ (global)
- Read-only.

Inputs:

| Field | Where | Type | Required | Notes |
| --- | --- | --- | --- | --- |
| `limit` | query | integer | no | Page size (1–100); default 20 |
| `after` | query | string | no | Opaque cursor from the previous page's `nextCursor`; omit for the first page |
| `search` | query | string | no | Case-insensitive match on name or email |
| `status` | query | enum: active \| disabled \| invited \| expired | no | Filter by computed status |
| `orderBy` | query | enum: email \| first_name \| last_name \| created_date \| last_login_date \| status | no | Sort column; default "created_date" |
| `sort` | query | enum: ASC \| DESC | no | default "DESC" |

## `reinvite_user` — Re-invite a user

Refresh the invite and resend the invite email for a pending (invited but not yet accepted) user — the caller becomes the inviter on every refreshed link, with today's date and a new expiry; the previous invite stays in the user's audit history; needs the users grant at the scope of every pending link it refreshes. 404 when the user is unknown or has already accepted. 409 `invite_account_mismatch` when the person's sign-in account does not match their Maison user (a Maison support fix; no email is sent); 502 `invite_account_unavailable` when the sign-in account cannot be checked (no email is sent; retry).

- REST: `POST https://maison-labs.com/api/v1/user/{userId}/reinvite`
- MCP tool / A2A skill: `reinvite_user`
- Needs: maison-users EXECUTE (any)

Inputs:

| Field | Where | Type | Required | Notes |
| --- | --- | --- | --- | --- |
| `userId` | path | string | yes | User id; format uuid |
| `inviteExpiryDays` | body | integer | no | default 7 |

## `remove_client_user` — Remove a user from a hotel

Soft-delete all direct role-links for a user at a hotel and end every session the user holds (they must sign in again). Group-inherited access is not affected. Returns the number of links removed.

- REST: `DELETE https://maison-labs.com/api/v1/client/{clientId}/user/{userId}`
- MCP tool / A2A skill: `remove_client_user`
- Needs: maison-users WRITE (client)
- Destructive — confirm with the user first.

Inputs:

| Field | Where | Type | Required | Notes |
| --- | --- | --- | --- | --- |
| `clientId` | path | string | yes | Client (hotel) id; format uuid |
| `userId` | path | string | yes | User id; format uuid |

## `resend_client_invite` — Resend a hotel invite

Re-invite a user to a hotel: refresh every pending (not yet accepted, open or expired) invite link at the hotel together — the caller becomes the inviter, with today's date and a new expiry — and send one invite email naming them. The new email's link replaces the earlier one (an earlier link no longer accepts). `destination` sets where the new link lands: the Admin Dashboard (`{ kind: 'admin' }`) or this hotel's demo site with the QA editor open (`{ kind: 'demo', preview_website_id }`, a `demoSites[].id` from `get_client_overview`; 404 when it is not this hotel's); without it API callers keep the current invitation's destination, and the Maison agent gets `status: "needs_input"` with the choices to ask the operator. Returns `{ sent, invitationFor }` — the product the new link opens (`'Admin Dashboard'` or `'Demo site (QA editor)'`). The previous invite (who, when, its expiry) stays in the user's audit history. 404 when the user or hotel is unknown. 409 `not_pending` when the user has no pending invite at the hotel (accepted, added directly, or never invited there). 502 `email_failed` when the links were refreshed but the email could not be sent (call again). 409 `invite_account_mismatch` when the person's sign-in account does not match their Maison user (a Maison support fix; no email is sent); 502 `invite_account_unavailable` when the sign-in account cannot be checked (no email is sent; retry).

- REST: `POST https://maison-labs.com/api/v1/client/{clientId}/user/{userId}/reinvite`
- MCP tool / A2A skill: `resend_client_invite`
- Needs: maison-users EXECUTE (client)

Inputs:

| Field | Where | Type | Required | Notes |
| --- | --- | --- | --- | --- |
| `clientId` | path | string | yes | Client (hotel) id; format uuid |
| `userId` | path | string | yes | User id; format uuid |
| `destination` | body | object | no | Where the new link lands — `{ kind: 'admin' }` the Admin Dashboard, or `{ kind: 'demo', preview_website_id }` this hotel's demo site with the QA editor open. API callers: omit to keep the current invitation's destination. The Maison agent must send it (without it the tool returns `status: "needs_input"` with the choices); ask the operator when they have not said. |

## `revoke_client_invite` — Revoke a pending hotel invite

Soft-delete all pending (not yet accepted) invite links for a user at a hotel. Idempotent — returns revoked count (0 when there are no pending links).

- REST: `DELETE https://maison-labs.com/api/v1/client/{clientId}/user/{userId}/invite`
- MCP tool / A2A skill: `revoke_client_invite`
- Needs: maison-users WRITE (client)
- Destructive — confirm with the user first.

Inputs:

| Field | Where | Type | Required | Notes |
| --- | --- | --- | --- | --- |
| `clientId` | path | string | yes | Client (hotel) id; format uuid |
| `userId` | path | string | yes | User id; format uuid |

## `revoke_user_connected_app` — Revoke a user's connected app

Revoke one of a user's OAuth consents (a grant id from `list_user_connected_apps`): the app's access tokens are refused within 30 seconds and it cannot refresh. Idempotent for an already revoked grant. Recorded in the app owner's audit log. 404 when the grant is unknown or belongs to someone else.

- REST: `POST https://maison-labs.com/api/v1/user/{userId}/connectedapp/{grantId}/revoke`
- MCP tool / A2A skill: `revoke_user_connected_app`
- Needs: maison-users WRITE (global)
- Destructive — confirm with the user first.

Inputs:

| Field | Where | Type | Required | Notes |
| --- | --- | --- | --- | --- |
| `userId` | path | string | yes | User id; format uuid |
| `grantId` | path | string | yes | Grant id |

## `set_client_user_groups` — Set a user's groups at a hotel

Replace all client-owned group memberships for a user at a specific hotel. Groups not in the new list are removed; groups in the new list are added if absent.

- REST: `PUT https://maison-labs.com/api/v1/client/{clientId}/user/{userId}/groups`
- MCP tool / A2A skill: `set_client_user_groups`
- Needs: maison-users WRITE (client)

Inputs:

| Field | Where | Type | Required | Notes |
| --- | --- | --- | --- | --- |
| `clientId` | path | string | yes | Client (hotel) id; format uuid |
| `userId` | path | string | yes | User id; format uuid |
| `groupIds` | body | array of string | yes | The complete set of client group ids |

## `set_client_user_roles` — Set a user's roles at a hotel

Replace all direct role-links for a user at a specific hotel. Existing links not in the new list are soft-deleted; new links are upserted. Pass an empty array to revoke all direct access.

- REST: `PUT https://maison-labs.com/api/v1/client/{clientId}/user/{userId}/roles`
- MCP tool / A2A skill: `set_client_user_roles`
- Needs: maison-users WRITE (client)

Inputs:

| Field | Where | Type | Required | Notes |
| --- | --- | --- | --- | --- |
| `clientId` | path | string | yes | Client (hotel) id; format uuid |
| `userId` | path | string | yes | User id; format uuid |
| `roleIds` | body | array of string | yes | The complete set of role ids to assign |

## `update_user` — Update a user

Patch a user's first name, last name (at most 25 characters each), or phone number. 404 when the user is unknown.

- REST: `PUT https://maison-labs.com/api/v1/user/{userId}`
- MCP tool / A2A skill: `update_user`
- Needs: maison-users WRITE (global)

Inputs:

| Field | Where | Type | Required | Notes |
| --- | --- | --- | --- | --- |
| `userId` | path | string | yes | User id; format uuid |
| `firstName` | body | string | no | At most 25 characters |
| `lastName` | body | string | no | At most 25 characters |
| `phone` | body | string \| null | no |  |
