Three control-plane calls manage people who already have an account. Each is
published in GET /openapi.json and in the capability matrix at
GET /api/v1/rbac/matrix.
Change a role grant#
PUT /api/v1/memberships/{id} changes the role of one grant in a single
statement.
| Field | Type | Meaning |
|---|---|---|
role | string | one of admin, member, viewer; required |
No other field is accepted. The scope of a grant cannot change: revoke it and grant a new one for that.
- Authorized as
membership:update(admin) at the grant’s own scope, the way a revoke is. A caller who is admin only of a team cannot change a grant on the organization. - Answers
200with the updated grant,400for an unknown role,404for an unknown grant. - Demoting the organization’s last active org-scoped
admingrant is refused with409and the stable codelast_org_admin, exactly as revoking it is. A superadmin is exempt. - Writes one
membership.updateaudit entry carryinguser_id,old_roleandnew_role. Sending the grant’s current role changes nothing and writes no entry. - A grant created by an SSO group mapping or SCIM becomes
manual, so the next reconcile does not undo the edit.
See who sent an invitation#
GET /api/v1/orgs/{org_id}/invitations returns invited_by_email on each row
next to invited_by. The control plane resolves it, so it is present whichever
users the caller may list: a team admin sees who sent an invitation into their
team even when the sender is an org admin they cannot list. It is null when
the invitation was sent with the admin token or its sender’s account was deleted
(in which case invited_by is null too). Only the list carries it; creating or
revoking an invitation returns it as null.
See whether an invitation was emailed#
Each invitation row carries mail_status (sent, failed, or not_configured
when the control plane has no mail relay), mail_attempted_at and, for a
failure, mail_error, a short reason that is safe to show. Creating an
invitation always returns the link, whatever the status says. See
Emailing invitations.
Send an invitation again#
POST /api/v1/invitations/{id}/resend emails a pending invitation’s link again.
It is the same link, so the earlier message still works, and the expiry does not
move. The request has no body and the response is the invitation, with
mail_status, mail_attempted_at and mail_error updated. It needs the same
role as sending the invitation, and is audited as invitation.resend followed
by invitation.mail_sent or invitation.mail_failed. A resend is refused with 429 and a Retry-After header while the invitation’s last mail attempt, the first send included, is under a minute old.
It answers 409 when the invitation was accepted, revoked or has expired, when
the control plane has no mail relay, and for an invitation whose link was not
kept: one created before resending existed, with no relay, or on a control plane
without ROLTER_KEK. Revoke that one and invite the address again.
Check the mail relay#
POST /api/v1/orgs/{org_id}/invitations/mail-check connects to the relay,
negotiates TLS and authenticates, without sending anything. It needs the role
that sends invitations in the organization.
The check is not rate limited. stage is connect (unreachable or no answer in time), tls, auth or
relay (it answered, but not as a mail server does). The reason never contains
the relay’s address, its reply or the credential. A control plane without
ROLTER_SMTP_URL answers 409. rolter check --connect runs the same check
from the command line.
Reset a member’s password#
POST /api/v1/orgs/{org_id}/users/{id}/password-reset issues a one-time link
for a member. The request has no body.
The response carries token and reset_url once; only a peppered digest of
the token is stored. reset is the row (id, user_id, created_by,
expires_at, created_at) and sessions_revoked is how many of the member’s
sessions ended. rolter does not send email: hand the link to the member.
An administrator never sees or chooses the password. The member opens
reset_url, a page of the dashboard, and sets their own:
GET /api/v1/password-reset/{token}previews the address the link is for;POST /api/v1/password-reset/{token}with{"password": "..."}stores it.
Both are unauthenticated (the token is the credential), throttled like a
password sign-in, and answer 401 for a link that is spent, replaced, expired
or wrong. Redeeming is single-use even under a race, ends every session of the
account again and signs nobody in: the member signs in normally, so an
armed second factor still applies. A reset never clears a second factor.
Issuing a link:
- needs
password_reset:create(admin) at the organization in the path. Below a superadmin the member must belong to that organization alone and must not be a superadmin, otherwise403: the link lets its holder choose a password for the whole account; - is refused with
400for your own account (use Change your password), an account with no local password (single sign-on, LDAP) and a deactivated account;404when the member holds no role in the organization; - ends the member’s sessions and pending second-factor sign-ins at once, and replaces the account’s earlier link;
- leaves the old password valid until the link is used; deactivate the account to cut it off sooner;
- writes a
user.password_resetaudit entry in the organization (issuer, member, sessions ended, the replaced link; never the token) and, when the link is used, auser.password_reset_completeentry against the account; - expires after 24 hours.
See Roles and permissions for the dashboard side.