Groundfloor Docs

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 paramValues
scope_typeaccount or workspace
scope_idOrganization 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}/members

Permission: manage_members on scope. The grantor cannot assign a role above their own ceiling on that scope.

Modes

BodyBehavior
email onlyInvite. 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 + passwordAdd: 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_idAdd 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

FieldTypeRequiredDescription
user_iduuidOne ofExisting Portal user ID
emailstringOne ofInvite or add by email (creates a Groundfloor login if needed)
passwordstringNoWith email only — set password now (8-128 characters)
temporary_passwordboolNoWhen password is set, require change on first sign-in (default true)
send_emailboolNoWhen password is set, whether to send the invitation-detail email (default true). Invites without a password always send email
roleenumYesA role valid for the scope, see Roles by scope
expires_atstringNoISO-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

ScopeValid roles
Organization (account)owner, admin, billing_admin, writer, member, partner
Workspaceowner, 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}/reject

GET …/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-password

Sends 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

PlaneDocumented in
Platform membership roles (owner, admin, writer, member, partner, billing_admin; see Roles by scope)This API + Team and roles
App end-user rolesAuthentication + Workspace auth API

Each endpoint checks a permission, such as read or manage_members, that a role grants. See Authentication.

Errors

StatusMeaning
400Invalid scope_type, both user_id and email, or password with user_id
403Missing manage_members, role exceeds grantor ceiling, or (on revoke) the member's role is equal to or above yours
404Membership or user not found
409Add: 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
422Role isn't valid for the scope (billing_admin on a workspace)