Agent Skills / maison-users

Install this skill alone, or read it below. Raw file: SKILL.md · references/tools.md

bash
npx skills add https://docs.maison-labs.com --skill 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

ToolWhat it doesRESTNeeds
add_client_userGive 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}/usermaison-users WRITE (client)
disable_userSoft-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}/disablemaison-users WRITE (global); destructive
disable_user_api_keyDisable 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_userRe-enable a disabled user account so they can sign in again (clears disabled_date).POST /api/v1/user/{userId}/enablemaison-users WRITE (global)
force_password_resetFlag 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}/forceresetmaison-users WRITE (global)
get_client_user_accessThe 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}/accessmaison-users READ (client); read-only
get_userFetch 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_activityOne 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}/activitymaison-users READ (global); read-only
invite_userCreate 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/invitemaison-users WRITE (any)
list_client_usersAll users with access to a specific hotel, both direct role-links and group-inherited memberships.GET /api/v1/client/{clientId}/usersmaison-users READ (client); read-only
list_credential_auditMaison staff only.GET /api/v1/audit/credentialsmaison-users READ (global); read-only
list_user_api_keysReturns the masked API-key records for one user (prefix + last-4 characters only — never the raw key).GET /api/v1/user/{userId}/apikeysmaison-users READ (global); read-only
list_user_clientsPage 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}/clientsmaison-users READ (global); read-only
list_user_connected_appsThe 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}/connectedappsmaison-users READ (global); read-only
list_usersPage through every Maison user with optional search, status filter, and sort.GET /api/v1/usersmaison-users READ (global); read-only
reinvite_userRefresh 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}/reinvitemaison-users EXECUTE (any)
remove_client_userSoft-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_inviteRe-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}/reinvitemaison-users EXECUTE (client)
revoke_client_inviteSoft-delete all pending (not yet accepted) invite links for a user at a hotel.DELETE /api/v1/client/{clientId}/user/{userId}/invitemaison-users WRITE (client); destructive
revoke_user_connected_appRevoke 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}/revokemaison-users WRITE (global); destructive
set_client_user_groupsReplace all client-owned group memberships for a user at a specific hotel.PUT /api/v1/client/{clientId}/user/{userId}/groupsmaison-users WRITE (client)
set_client_user_rolesReplace all direct role-links for a user at a specific hotel.PUT /api/v1/client/{clientId}/user/{userId}/rolesmaison-users WRITE (client)
update_userPatch 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.

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.