User Administration
This page walks through the day-to-day user-management operations. For what the roles and scopes mean, read RBAC & Authentication first. The full endpoint reference (request/response schemas, error shapes) is in the API appendix; this guide covers the operations an administrator actually performs, in the order they usually come up.
The examples assume:
- API:
$VSSis your API base URL and$TOKENa session JWT. Errors come back as RFC 7807 problem documents (application/problem+json). - vsscli: a configured profile (
vsscli profile create, or just log in — the profile is saved for you). Most commands default their--scope/--owner-scopeto the profile’s active namespace, so the examples pass scopes explicitly where it matters. Add--output jsonto any command for machine-readable output.
Logging in
Section titled “Logging in”With an API key (service accounts, automation)
Section titled “With an API key (service accounts, automation)”TOKEN=$(curl -s -X POST "$VSS/v1/login" \ -H "Authorization: Bearer $VSS_API_KEY" | jq -r .token)The JWT is valid for 1 hour; re-run the login when you receive a 401.
vsscli auth login --apikey # prompts for the key# or non-interactively:VSSCLI_API_KEY=vss_... vsscli auth login --apikeyThe token is stored in the active profile; vsscli auth status shows who
you are and when the token expires, and vsscli auth token print emits the
raw JWT (for handing to curl).
With a password (interactive staff login)
Section titled “With a password (interactive staff login)”Password login is scoped to the partition or namespace that owns your user account, and always involves the second factor.
# Step 1: password → 2FA challengecurl -s -X POST "$VSS/v1/pt/$PTID/login" \ -d '{"username":"alice","password":"…"}'# → {"success":true,"challenge_id":"…","expires_at":"…","type":"2fa-required"}
# Step 2: TOTP code (or a recovery code) → full tokenTOKEN=$(curl -s -X POST "$VSS/v1/pt/$PTID/login/2fa" \ -d '{"challenge_id":"…","code":"123456"}' | jq -r .token)A brand-new account (no TOTP enrolled yet) instead returns
"type":"bootstrap" in step 1 — a 5-minute token valid only for the
/v1/me/totp/enroll endpoints. Enroll, then repeat the login.
Namespace-owned users use /v1/ns/$NSID/login and /v1/ns/$NSID/login/2fa.
vsscli login --user alice --partition $PTID# Password: ********# TOTP code: 123456The CLI handles every branch of the flow: on a first login it walks you
through TOTP enrollment (QR code in the terminal, recovery codes printed
once), then completes the login. Namespace-owned users pass
--namespace $NSID instead of --partition.
To check who you currently are:
curl -s "$VSS/v1/me" -H "Authorization: Bearer $TOKEN"vsscli meCreating a user
Section titled “Creating a user”Creating a staff or subscriber account is three separate steps by design: create the record, attach a credential, add grants. A freshly created user exists but can neither log in (no credential) nor do anything (no grants).
1. Create the record
Section titled “1. Create the record”curl -s -X POST "$VSS/v1/user" \ -H "Authorization: Bearer $TOKEN" \ -d '{ "username": "bob", "email": "bob@example.net", "display_name": "Bob Ops", "owner_scope": {"st": "partition", "si": "'$PTID'"} }'# → {"success":true,"user_id":"<uuid>"}owner_scope.st is partition or namespace, with si the scope’s UUID.
Remember: the owner scope is immutable — pick the scope whose admins
should own this account.
vsscli user create --username bob --email bob@example.net \ --display-name "Bob Ops" --owner-scope partition:$PTID# Created user 0198… (owner scope: partition:…)Scopes are written partition:<uuid> (alias pt:) or namespace:<uuid>
(alias ns:). Omitting --owner-scope uses the profile’s active
namespace.
2. Set a password (or mint an API key)
Section titled “2. Set a password (or mint an API key)”curl -s -X PUT "$VSS/v1/user/$USER_ID/password" \ -H "Authorization: Bearer $TOKEN" \ -d '{"new_password":"a-strong-initial-password"}'vsscli user password set $USER_ID # prompts for the passwordHand the initial password to the user out of band; their first login forces TOTP enrollment before they get a working session. For a service account, skip the password and mint an API key instead (below).
3. Grant a role
Section titled “3. Grant a role”curl -s -X PUT "$VSS/v1/user/$USER_ID/grant" \ -H "Authorization: Bearer $TOKEN" \ -d '{ "scope_type": "namespace", "scope_id": "'$NSID'", "role": "operator", "applies_to": "subtree" }'Optional: "expiry": "2026-10-01T00:00:00Z" (RFC 3339) for a
self-revoking grant.
vsscli user grant add $USER_ID --role operator \ --scope ns:$NSID --applies-to subtree# Granted operator on namespace:… to …--expiry 2026-10-01T00:00:00Z adds a time limit.
Remember the two-sided delegation rule: you need user-management authority over the user’s owner scope and you must hold the role (or a full-admin role) over the grant’s scope. A 401 on grant-add almost always means the second condition failed.
Service accounts and API keys
Section titled “Service accounts and API keys”API keys are named per-user credentials: PUT mints (or rotates — same
name replaces the key), GET lists metadata, DELETE revokes.
# Mint / rotate (plaintext returned exactly once)curl -s -X PUT "$VSS/v1/user/$USER_ID/apikey/ci-deploy" \ -H "Authorization: Bearer $TOKEN" \ -d '{"label":"CI deploy key","expiry":"2027-01-01T00:00:00Z"}'# → {"success":true,"credential_id":"ci-deploy","api_key":"vss_…"}
# List (metadata only — no secrets)curl -s "$VSS/v1/user/$USER_ID/apikeys" -H "Authorization: Bearer $TOKEN"
# Revokecurl -s -X DELETE "$VSS/v1/user/$USER_ID/apikey/ci-deploy" \ -H "Authorization: Bearer $TOKEN"vsscli user apikey create $USER_ID ci-deploy \ --label "CI deploy key" --expiry 2027-01-01T00:00:00Zvsscli user apikey list $USER_IDvsscli user apikey delete $USER_ID ci-deployUsers can manage their own keys without any admin grant — the same
endpoints with your own user id are self-permitted. A sensible automation
pattern: one service user per integration, one named key per environment,
expiries set, rotation = re-PUT the same name.
Inspecting and listing users
Section titled “Inspecting and listing users”# One user, full admin view (grants + credential metadata)curl -s "$VSS/v1/user/$USER_ID" -H "Authorization: Bearer $TOKEN"
# All users owned by a scope (cursor-paginated, default 100/max 500)curl -s "$VSS/v1/users?scope_type=partition&scope_id=$PTID&limit=200" \ -H "Authorization: Bearer $TOKEN"# → {"users":[…],"next_cursor":"<user-id>"} — pass back as &after=vsscli user get $USER_IDvsscli user get --username bob --owner-scope pt:$PTIDvsscli user list --owner-scope pt:$PTID --limit 200# next page:vsscli user list --owner-scope pt:$PTID --after <last-user-id>Every command that targets a single user accepts --username in place of
the user-id argument (user get/delete, user grant add/remove/list,
user password set/delete, user totp delete, user apikey create/list/delete — the API-key commands then take just the key name).
The lookup searches one owner scope (--owner-scope, defaulting to the
active namespace) and refuses ambiguous matches. user create and
user update are the exception: there --username is the username
value being set.
Note that listing is by owner scope — “everyone owned by this partition” — not by grant. To answer “who has access to this namespace?”, query grants by scope instead:
curl -s "$VSS/v1/grants?scope_type=namespace&scope_id=$NSID" \ -H "Authorization: Bearer $TOKEN"# → {"holders":[{"user_id":"…","role":"operator","granted_by":"…","granted_at":"…"}]}vsscli grants list --scope ns:$NSIDThis is the access-review view: it includes who granted each role and when. Remember its blind spot — a user can also reach this namespace via a grant on an ancestor namespace or on the partition, so review those scopes too.
Reviewing and revoking a user’s grants
Section titled “Reviewing and revoking a user’s grants”# What does this user hold? (includes granted_by / granted_at provenance)curl -s "$VSS/v1/user/$USER_ID/grants" -H "Authorization: Bearer $TOKEN"
# Revoke: the same (scope, role) tuple used to create itcurl -s -X DELETE "$VSS/v1/user/$USER_ID/grant" \ -H "Authorization: Bearer $TOKEN" \ -d '{"scope_type":"namespace","scope_id":"'$NSID'","role":"operator"}'vsscli user grant list $USER_IDvsscli user grant remove $USER_ID --role operator --scope ns:$NSIDRevocation takes effect on the user’s next login — an existing JWT carries the grants it was minted with, for up to the remaining hour of its life. To cut access immediately, disable the user (below): disabled users fail credential lookup on every new login, and you can re-enable after the token horizon has passed.
Credential resets
Section titled “Credential resets”Password reset (admin)
Section titled “Password reset (admin)”curl -s -X PUT "$VSS/v1/user/$USER_ID/password" \ -H "Authorization: Bearer $TOKEN" \ -d '{"new_password":"new-temporary-password"}'vsscli user password set $USER_IDNo old password is needed on the admin path. Users change their own with
POST /v1/me/change-password / vsscli me password change, which does
require the current one.
Lost authenticator (TOTP reset)
Section titled “Lost authenticator (TOTP reset)”First choice: the user logs in with one of their recovery codes in
place of the TOTP code, then re-enrolls at leisure (vsscli me totp delete
then enroll). When the recovery codes are gone too, an admin removes the
TOTP credential — this also invalidates the remaining recovery codes, and
the user’s next password login lands in the enrollment flow again:
curl -s -X DELETE "$VSS/v1/user/$USER_ID/totp" \ -H "Authorization: Bearer $TOKEN"vsscli user totp delete $USER_IDDisabling and deleting a user
Section titled “Disabling and deleting a user”# Disable (reversible; blocks all new logins)curl -s -X PUT "$VSS/v1/user/$USER_ID" \ -H "Authorization: Bearer $TOKEN" \ -d '{"disabled": true}'
# Delete (permanent; removes credentials, grants, API keys)curl -s -X DELETE "$VSS/v1/user/$USER_ID" \ -H "Authorization: Bearer $TOKEN"The update endpoint has patch semantics — only the fields you send change
(username, email, display_name, disabled).
vsscli user update $USER_ID --disabled truevsscli user delete $USER_ID # prompts for confirmationOffboarding pattern: disable immediately (kills new logins now), then delete after your retention window. Deleting or disabling your own account is refused with a 409 Conflict — an admin cannot lock themselves out mid-session (re-enabling yourself is allowed, should another admin have disabled you). There is, however, no last-admin-standing guard between different accounts: one admin can still delete or disable the only other admin of a partition, so keep at least two admin accounts (or a break-glass API-key service user) per partition.
Setting up a helpdesk
Section titled “Setting up a helpdesk”The helpdesk role is built for tier-1 subscriber support: it can read
users, edit profile fields, and reset passwords/credentials — but only on
self-service accounts (users whose every grant is a feature role such as
voicemail-owner or line-owner). Pointing it at a staff account fails
with 401 even though the role nominally has user:credential:write.
# The subscriber account a portal would create:vsscli user create --username sub-1001 --owner-scope ns:$NSIDvsscli user grant add <sub-user-id> --role line-owner \ --scope "resource:$NSID!line!1001"vsscli user grant add <sub-user-id> --role voicemail-owner \ --scope "resource:$NSID!vm_box!default!1001"
# The helpdesk operator who may reset such accounts (and nothing else):vsscli user create --username helpdesk-dana --owner-scope ns:$NSIDvsscli user grant add <dana-user-id> --role helpdesk --scope ns:$NSIDDana can now vsscli user password set <sub-user-id> but gets a 401
attempting the same against your operator accounts.
Troubleshooting a 401
Section titled “Troubleshooting a 401”Both “who are you” and “you may not” failures surface as 401 problem documents, so work through them in order:
- Token problems — expired (1-hour lifetime), or a bootstrap token
being used outside TOTP enrollment. Re-login;
vsscli auth statusshows the expiry. - Coverage — none of your grants reaches the target: wrong partition,
a namespace grant used against a partition-level endpoint, an
applies_to: descendantsgrant used on the granted node itself, or an expired grant. - Policy — your role reaches the target but doesn’t allow the action
(an
editorcalling apply, aviewerwriting). The role catalog on the RBAC page lists each role’s action surface. - Special rules — grant delegation (you don’t control the grant’s landing scope), helpdesk constraint (target isn’t a self-service user), or a self-mutation exclusion (disabling/deleting yourself).
The server logs every denial with the candidate roles it considered, which pinpoints whether you failed at coverage (no roles) or policy (roles listed, action refused).