Memberships API
List, add, and revoke members on organization or workspace scopes.
Membership APIs control who has which role on an organization or workspace.
See also: Team and roles
List members
GET /v1/scopes/{scope_type}/{scope_id}/members| Path param | Values |
|---|---|
scope_type | account or workspace |
scope_id | Organization or workspace UUID |
Permission: read on scope
curl -s -H "Authorization: Bearer $TOKEN" \
"https://platform.groundfloor.cloud/v1/scopes/workspace/$WORKSPACE_ID/members" | jq .Response 200
{
"memberships": [
{
"id": "…",
"user_id": "…",
"scope_type": "workspace",
"scope_id": "…",
"role": "admin",
"status": "accepted",
"user_email": "admin@example.com",
"user_display_name": "Admin User",
"created_at": "2026-05-01T12:00:00Z"
}
]
}The list returns at most 200 members. status is accepted or pending. A pending row is an email invite the invitee has not accepted yet — it grants no access until they do.
Each row also has user_idp_realm: null for your team. A non-null value marks a leftover row for one of your app's users, from before app users were kept apart from your team; the Client Portal hides these rows and they are being removed.
Add / invite member
POST /v1/scopes/{scope_type}/{scope_id}/membersPermission: manage_members on scope. The grantor cannot assign a role above their own ceiling on that scope.
Modes
| Body | Behavior |
|---|---|
email only | Invite. Existing Portal user: role granted now. New user: a Groundfloor login is created, a set-password email is sent, membership created as pending — no access until they accept |
email + password | Add: set credentials now and grant the role now. An invitation-detail email (never containing the password) is still sent unless you pass send_email: false |
user_id | Add an existing Portal user (no password field) |
If a Portal user already exists for that email and you omit password, they are added and emailed the invitation details. If you pass password and the email already has credentials, the API returns an error — invite without password or use user_id.
# Invite (set-password email)
curl -s -X POST -H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"email":"colleague@company.com","role":"writer"}' \
"https://platform.groundfloor.cloud/v1/scopes/workspace/$WORKSPACE_ID/members" | jq .
# Add with password (no email at all)
curl -s -X POST -H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"email":"colleague@company.com","password":"••••••••","temporary_password":true,"send_email":false,"role":"writer"}' \
"https://platform.groundfloor.cloud/v1/scopes/workspace/$WORKSPACE_ID/members" | jq .Request body
| Field | Type | Required | Description |
|---|---|---|---|
user_id | uuid | One of | Existing Portal user ID |
email | string | One of | Invite or add by email (creates a Groundfloor login if needed) |
password | string | No | With email only — set password now (8-128 characters) |
temporary_password | bool | No | When password is set, require change on first sign-in (default true) |
send_email | bool | No | When password is set, whether to send the invitation-detail email (default true). Invites without a password always send email |
role | enum | Yes | A role valid for the scope, see Roles by scope |
expires_at | string | No | ISO-8601 expiry |
Provide exactly one of user_id or email. password is only valid with email.
Returns 201 with the membership plus email_sent and email_skipped_reason. email_sent is true (sent), false (not sent) or null (the mail server didn't confirm in time; it may still arrive). email_skipped_reason is smtp_not_configured, send_failed, delivery_unconfirmed, not_requested or null; keycloak_smtp_missing is no longer returned. Adding a user who already holds the same role on the scope returns the existing membership; a different role returns 409 (change it with PATCH).
Roles by scope
| Scope | Valid roles |
|---|---|
Organization (account) | owner, admin, billing_admin, writer, member, partner |
| Workspace | owner, admin, writer, member, partner |
billing_admin exists only at organization scope; on a workspace it's 422. partner is stored as a distinct role but grants the same permissions as member. Organization owner and admin hold administer on every workspace in the organization; organization writer, member and partner can read every workspace in it. billing_admin is billing only: it can read the organization and its member list and has view_billing, but can't manage members and gets nothing on any workspace. See Permissions.
Role ranks for granting and revoking are owner > admin > billing_admin = writer > member = partner. Only an owner grants owner. An organization admin can grant up to admin and revoke only roles below admin.
Agent hint
Prefer invite (email + role) unless the operator explicitly asked to set a password. After success (after acceptance, for a pending invite), platform vault access follows the new workspace role — Shell end users still need collection ACL.
Accept or decline an invitation
The invitee (signed-in, own invitations only) lists and answers pending invitations:
GET /v1/me/pending-invitations
POST /v1/memberships/{membership_id}/accept
POST /v1/memberships/{membership_id}/rejectGET …/pending-invitations returns membership_id, scope_type, scope_id, scope_name, role, inviter_name, created_at. accept grants the role and returns { "status": "accepted" }. reject deletes the membership (204); their Groundfloor login is kept.
Someone else's invitation, or one that is not pending, returns 404. Accepting one that was already accepted concurrently returns 409.
Get a membership
GET /v1/memberships/{membership_id}Returns the membership with the member's profile (user_status, user_created_at, welcome_email_sent_at, last_login_at). Permission: manage_members on the membership's scope.
Change a role
PATCH /v1/memberships/{membership_id}{ "role": "admin" }Changes the role in place; there is no need to revoke and re-add. The role must be valid for the scope, and you must be allowed both to grant the new role and to revoke the current one (an owner can't demote another owner, and the last owner can't be demoted). Returns the updated membership. Like adding a member, this is refused while the workspace is frozen. Permission: manage_members on the membership's scope.
Send a password reset
POST /v1/memberships/{membership_id}/reset-passwordSends the member a set-password email. Returns 202 with { "status": "accepted", "email_sent": true, "email_skipped_reason": null }. Only available for Client Portal (platform sign-in) users; others get 409. Permission: manage_members on the membership's scope.
Revoke membership
DELETE /v1/memberships/{membership_id}Permission: manage_members on the membership's scope. You can only revoke a role ranked strictly below your own on the scope, and the last owner of a scope can't be removed. Revoking still works on a frozen workspace, so you can remove a compromised member.
curl -s -X DELETE -H "Authorization: Bearer $TOKEN" \
"https://platform.groundfloor.cloud/v1/memberships/$MEMBERSHIP_ID" -w "%{http_code}\n"Returns 204 No Content on success.
Roles vs permissions
| Plane | Documented in |
|---|---|
Platform membership roles (owner, admin, writer, member, partner, billing_admin; see Roles by scope) | This API + Team and roles |
| App end-user roles | Authentication + Workspace auth API |
Each endpoint checks a permission, such as read or manage_members, that a role grants. See Authentication.
Errors
| Status | Meaning |
|---|---|
| 400 | Invalid scope_type, both user_id and email, or password with user_id |
| 403 | Missing manage_members, role exceeds grantor ceiling, or (on revoke) the member's role is equal to or above yours |
| 404 | Membership or user not found |
| 409 | Add: the user already has a different role on this scope (use PATCH to change it), or the organization's sign-in group is linked to another organization (contact Groundfloor support). Revoke: the member is the last owner. Reset password: the user doesn't use platform sign-in |
| 422 | Role isn't valid for the scope (billing_admin on a workspace) |