---
name: "maison-users"
description: "Manage people on Maison: list and read platform users, invite new users with roles and groups in one call, update, disable, enable, re-invite or force a password reset, read a user's activity, hotels, masked API keys and connected apps (and disable a key or revoke an app); and manage a hotel's own users — add a person to a hotel with roles (a new email gets an account in the same call), change their roles or groups, resend or revoke a hotel invite, remove them. Use for onboarding staff, access reviews and offboarding. Tools: add_client_user, disable_user, disable_user_api_key, enable_user, force_password_reset, get_client_user_access, get_user, get_user_activity, invite_user, list_client_users, list_credential_audit, list_user_api_keys, list_user_clients, list_user_connected_apps, list_users, reinvite_user, remove_client_user, resend_client_invite, revoke_client_invite, revoke_user_connected_app, set_client_user_groups, set_client_user_roles, update_user."
license: "Proprietary. Use with a Maison API key under the Maison terms of service."
metadata:
  author: "maison-labs"
  version: "1.0.0"
  category: "users"
  api-base: "https://maison-labs.com"
  openapi: "https://maison-labs.com/api/v1/openapi.json"
  docs: "https://docs.maison-labs.com/skills/maison-users/"
---

# Maison — Users and invitations

Manage people on Maison: list and read platform users, invite new users with roles and groups in one call, update, disable, enable, re-invite or force a password reset, read a user's activity, hotels, masked API keys and connected apps (and disable a key or revoke an app); and manage a hotel's own users — add a person to a hotel with roles (a new email gets an account in the same call), change their roles or groups, resend or revoke a hotel invite, remove them. Use for onboarding staff, access reviews and offboarding.

Part of the Maison public API. Read the `maison-public-api` skill first for authentication, discovery and transport details (`https://docs.maison-labs.com/skills/maison-public-api/SKILL.md`).

## When to use

- Before creating a user (`add_client_user`, `invite_user`) ask the operator what they have not said: "Create only, or send an invitation? If inviting, should the link open the Admin Dashboard or the demo site with the QA editor?" Never default either silently.
- Give a hotel employee access — `add_client_user` (any email: an existing user is invited, a new one gets an account and the invite), or `invite_user` for roles at several hotels or Maison-wide.
- To add someone without any email (they cannot open an invite link yet), call `invite_user` with `mode: "create"` and `auth` (base64 of a password of at least 8 characters): the account is active at once with that password, and you share it with them yourself. An invite (the default mode) refuses `auth`.
- Audit who can act on a hotel — `list_client_users`, `get_client_user_access`.
- Offboard — `remove_client_user` (one hotel) or `disable_user` (platform-wide, Maison staff).
- Offboard completely — `disable_user`, then `revoke_user_connected_app` for any app still connected (`list_user_connected_apps`); `enable_user` undoes a disable.
- See every hotel a person can reach — `list_user_clients` (paged with `cursor`).
- Find out who reset a password, minted or disabled an API key, or revoked an app, and when — `list_credential_audit` (every user, Maison staff) or `get_user_activity` with `source: credentials` (one user).
- Reconstruct what one person did and when — `get_user_activity` with a `from` / `to` window (at most 366 days): who invited them, their sign-ins and sign-outs, every record they created, changed or deleted, and their hotel activity, in plain words with console links.

## Before you start

1. Send `x-api-key: <key>` on every call; the key only reaches `/api/v1`.
2. `GET /api/v1/tools` (or MCP `tools/list`) returns the tools THIS key may run — if a tool below is missing there, you lack a grant; do not retry it.
3. Client-scoped tools take `clientId` (a hotel id). `whoami` lists the ids you can act on.

## Tools

| Tool | What it does | REST | Needs |
| --- | --- | --- | --- |
| `add_client_user` | Give a person access to a hotel with roles and groups, in one call, whether or not they have a Maison account. | `POST /api/v1/client/{clientId}/user` | maison-users WRITE (client) |
| `disable_user` | Soft-disable a user account (sets disabled_date; the sign-in account is NOT deleted) and stop their API keys at once. | `POST /api/v1/user/{userId}/disable` | maison-users WRITE (global); destructive |
| `disable_user_api_key` | Disable one of a user's API keys; calls with it are refused at once. | `DELETE /api/v1/user/{userId}/apikey/{keyId}` | api-keys WRITE (global); destructive |
| `enable_user` | Re-enable a disabled user account so they can sign in again (clears disabled_date). | `POST /api/v1/user/{userId}/enable` | maison-users WRITE (global) |
| `force_password_reset` | Flag a user's account so they must set a new password, and email them a link to the reset-code page. | `POST /api/v1/user/{userId}/forcereset` | maison-users WRITE (global) |
| `get_client_user_access` | The full set of feature permissions a user holds at a specific hotel — direct roles and group-inherited grants combined. | `GET /api/v1/client/{clientId}/user/{userId}/access` | maison-users READ (client); read-only |
| `get_user` | Fetch a user's full profile: identity fields, computed status, direct and inherited roles, groups, client access, and masked API-key list. | `GET /api/v1/user/{userId}` | maison-users READ (global); read-only |
| `get_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`). | `GET /api/v1/user/{userId}/activity` | maison-users READ (global); read-only |
| `invite_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. | `POST /api/v1/user/invite` | maison-users WRITE (any) |
| `list_client_users` | All users with access to a specific hotel, both direct role-links and group-inherited memberships. | `GET /api/v1/client/{clientId}/users` | maison-users READ (client); read-only |
| `list_credential_audit` | Maison staff only. | `GET /api/v1/audit/credentials` | maison-users READ (global); read-only |
| `list_user_api_keys` | Returns the masked API-key records for one user (prefix + last-4 characters only — never the raw key). | `GET /api/v1/user/{userId}/apikeys` | maison-users READ (global); read-only |
| `list_user_clients` | Page through the hotels a user can act on through their direct roles, one row per (hotel, role), ordered by hotel name. | `GET /api/v1/user/{userId}/clients` | maison-users READ (global); read-only |
| `list_user_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. | `GET /api/v1/user/{userId}/connectedapps` | maison-users READ (global); read-only |
| `list_users` | Page through every Maison user with optional search, status filter, and sort. | `GET /api/v1/users` | maison-users READ (global); read-only |
| `reinvite_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. | `POST /api/v1/user/{userId}/reinvite` | maison-users EXECUTE (any) |
| `remove_client_user` | Soft-delete all direct role-links for a user at a hotel and end every session the user holds (they must sign in again). | `DELETE /api/v1/client/{clientId}/user/{userId}` | maison-users WRITE (client); destructive |
| `resend_client_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. | `POST /api/v1/client/{clientId}/user/{userId}/reinvite` | maison-users EXECUTE (client) |
| `revoke_client_invite` | Soft-delete all pending (not yet accepted) invite links for a user at a hotel. | `DELETE /api/v1/client/{clientId}/user/{userId}/invite` | maison-users WRITE (client); destructive |
| `revoke_user_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. | `POST /api/v1/user/{userId}/connectedapp/{grantId}/revoke` | maison-users WRITE (global); destructive |
| `set_client_user_groups` | Replace all client-owned group memberships for a user at a specific hotel. | `PUT /api/v1/client/{clientId}/user/{userId}/groups` | maison-users WRITE (client) |
| `set_client_user_roles` | Replace all direct role-links for a user at a specific hotel. | `PUT /api/v1/client/{clientId}/user/{userId}/roles` | maison-users WRITE (client) |
| `update_user` | Patch a user's first name, last name (at most 25 characters each), or phone number. | `PUT /api/v1/user/{userId}` | maison-users WRITE (global) |

Full input fields for every tool: [references/tools.md](references/tools.md).

## Recommended workflows

### Add someone to a hotel

1. Ask the operator both choices they have not stated, then map them: create only = `mode: "create"` with `auth`; an invite = `mode: "invite"` with `destination` `{ kind: "admin" }` (Admin Dashboard) or `{ kind: "demo", preview_website_id }` (demo site with the QA editor; a `demoSites[].id` from `get_client_overview`).
2. `list_client_roles` for the hotel to pick the role ids to assign.
3. `add_client_user` with the email, `roleIds` (and/or `groupIds`; optional `firstName` / `lastName` for a new account), `mode` and, on an invite, `destination`. It invites an existing user or creates and invites a new one — no second call. Invites are per hotel: a person who already has accepted access at this hotel gets the roles added directly (`kind: "attached"`, no email). 409 means the person is disabled, already has every chosen role here, or is only invited here so far (`resend_client_invite`). A refused role (403 "… cannot be assigned here", also from `set_client_user_roles`) is not available at this hotel, retired, or Maison-only: pick from `list_client_roles` — roles available to all clients or to this hotel.
4. `get_client_user_access` to confirm the effective access.

## Tips

- `get_user` is one shot: profile + roles + groups + clients.
- `list_user_api_keys` returns masked keys only; keys cannot be created through the public API. `disable_user_api_key` stops another person's key (needs `api-keys` WRITE global); your own: `disable_my_api_key`.
- `list_credential_audit` pages every credential and account event (password set or reset, API key created or disabled, connected app revoked, user invited, created, enabled or disabled) newest first with the exact time, the acting user, how they authenticated and where from; filter by `type`, `category`, `actorUserId`, `targetUserId` and a `from`/`to` window. It never carries a password or a full key — only the masked form.

## Pitfalls

- Every users tool takes the `maison-users` grant — at the hotel for the `*_client_user*` tools and for roles given at a hotel (`invite_user`, `assign_role`), globally for links at all clients and for `update_user`, `disable_user`, `force_password_reset` and the platform directory. `clients` WRITE grants nothing here.
- A pending invite grants nothing until accepted — a 'user cannot see the hotel' report is often an unaccepted invite; `resend_client_invite` re-sends that hotel's invite (409 `not_pending` when nothing waits there; 502 `email_failed` when the email could not be sent — call again). To change where a sent invite opens (Admin Dashboard or the demo site with the QA editor), resend it with `destination`: the new link replaces the earlier one. Ask the operator which destination before resending when they have not said it; without `destination` the tool answers a needs-input result listing the choices.
- `add_client_user` answers 502 `email_failed` when the invitation was saved but the email could not be sent — do not call it again (409); call `resend_client_invite`.
- Inviting an existing person (`add_client_user`, `reinvite_user`, `resend_client_invite`) checks their sign-in account first and sends nothing when it fails: 409 `invite_account_mismatch` means the sign-in account does not match their Maison user — calling again will not help, it needs a Maison support fix; 502 `invite_account_unavailable` means the account could not be checked — retry later.
- `disable_user` on an already disabled user is 409, not a no-op.
- Setting another person's password directly is not a public tool: it is the admin console's, for Maison admins only (`maison-users` WRITE global), and is audited. Use `force_password_reset` — the person sets their own.
- `list_credential_audit` needs `maison-users` READ at GLOBAL scope; a grant at one hotel is refused (403) because the audit spans every hotel's people.

## Calling conventions

Same tool, three transports — pick whichever your runtime already speaks. Always send `x-api-key`.

```bash
# REST
curl -X POST "https://maison-labs.com/api/v1/client/<clientId>/user" -H "x-api-key: $MAISON_API_KEY" -H "content-type: application/json" -d '{ "email": … }'
```

```json
// MCP (https://maison-labs.com/mcp, Streamable HTTP): tools/call
{ "jsonrpc": "2.0", "id": 1, "method": "tools/call", "params": { "name": "add_client_user", "arguments": { "email": … } } }
```

```json
// A2A (https://maison-labs.com/a2a): tasks/send with a data part
{ "jsonrpc": "2.0", "id": 1, "method": "tasks/send", "params": { "message": { "role": "user", "parts": [{ "type": "data", "data": { "tool": "add_client_user", "input": { "email": … } } }] } } }
```

Errors are the same everywhere: `401` no/invalid key; `403 forbidden` with `details.missing` (the grants you lack — fall back to the composite's parts or stop); `403 not_entitled` (the hotel's plan lacks the feature); `404`, `409`, `413 too_large`, `503`. Inputs that fail validation return `400 invalid_input` with the Zod issues — fix and retry once.
