Organization Membership and Invitations

The workflow for joining organizations, accepting invitations, and managing membership-aware app state.

Organization membership identifies which organizations a user belongs to. It does not replace the authorization checks on protected organization and spatial operations.

The membership shell contains only the organization ID, organization name, and the caller's own membership record. GET /v1/orgs/{organizationId} returns description, createdAt, and updatedAt only with OrganizationView; otherwise those protected fields are null. Directory responses keep hasAccess as the membership-shell indicator and expose the independent hasProtectedInformationAccess flag. A personal access token further narrows every organization request to its current authoritative organization scopes and capability ceilings.

Without a membership shell, organization detail returns the same 404 used for an unknown organization; a cluster-local legacy Contributor grant does not confirm the parent organization through this route.

GET /v1/orgs/me/memberships returns active memberships only. Its includeClusterScope query parameter remains accepted for client compatibility, but a direct legacy Contributor share stays cluster-local and does not expose the owning organization's shell.

Each returned membership includes camerasAvailable. Use this organization-wide value to show or hide camera navigation before opening a location. It combines deployment availability, active billing and the organization's camera entitlement. It does not grant access: camera endpoints still check the caller's location permissions.

Create an organization

Call POST /v1/orgs with a name and optional description. The current user becomes a protected owner and receives an Owner-valued compatibility membership shell so legacy routes remain usable during rollout. The shell is not the source of authority for the new evaluator; the protected-owner relationship is.

Creation captures the platform's current default published entitlement version. The organization, protected owner, shell, exact version pin, authorization versions, audit record, and derived-work event commit together. If the default is missing, retired, or incompatible, creation fails without leaving a partial or unpinned organization. This user route does not accept an entitlement-version choice; explicit version selection belongs to the separately authorized platform administration workflow.

Chat/bootstrap setup is post-commit and asynchronous. It runs only if the owner's final OrganizationView and IntegratedChatUse results, including the current entitlement ceiling and deployment prerequisite, allow the operation. A version with chat disabled still creates the organization successfully and does not create chat state.

List Members

Call GET /v1/orgs/{organizationId}/members to load the complete member directory. offset and limit provide pagination; includeProfilePicture=true adds temporary profile-picture download links.

The caller must have OrganizationMemberRosterView. Membership and legacy role values alone are insufficient; a denied read returns 403 Forbidden. Returned role values are compatibility metadata, not authorization decisions.

Organization-management screens should instead call GET /v1/orgs/{organizationId}/roster. This endpoint combines real members and active pending invitations before applying search, kind/group filters, deterministic sorting, and pagination. It returns separate memberTotal and pendingInvitationTotal values. Keep using the member-only endpoint for workflows that can select only registered users.

Each roster item has a stable discriminated entryId and a kind of Member or Invitation. Invitation rows include the target email, expiry, current delivery state, current intended groups, concurrency version, and bounded action flags. Group details are omitted when the caller lacks OrganizationGroupsView. Action flags are presentation hints only; every mutation is authorized again by the server.

Discover Users

Without search, GET /v1/users exposes the caller and users from organizations where the exact current credential has OrganizationMemberRosterView. GET /v1/users/{userId} uses the same known-account boundary; a hidden direct lookup returns 404 Not Found. An active membership shell—even one carrying the compatibility Owner role—does not by itself expose other member identities or contact fields.

A trimmed search of at least three characters expands discovery to every active, non-rejected account. Known accounts can match username, display name, email, company, address, or phone and return their authorized profile fields. External accounts match only by username and return:

  • id
  • username
  • isKnown: false
  • profilePicture when includeProfilePicture=true

Their display name, email, company, address, phone numbers, and language are blank or null. Those hidden values do not influence matching, ranking, pagination, or hasNextPage.

The onlyKnown query parameter remains accepted for client compatibility; it does not disable global discovery for a qualifying search. Personal access token organization scopes and capability ceilings still control which accounts are known. Platform administrators retain separate explicit global-directory authority.

Directory visibility is a live authorization query rather than a materialized membership-derived access list. Permission and tier changes advance the durable Directory invalidation generation and record terminal convergence only after that generation is visible. Every request still computes the exact current actor boundary before database search and pagination, so a stale membership, role, or client page cannot preserve directory access.

The organization-name directory follows the same shell boundary for ordinary users: GET /v1/orgs never broadens beyond the caller's active membership shells, including when onlyKnown=false. Platform administrators may use onlyKnown=false for the global organization-name directory.

Read Legacy Entitlements

GET /v1/orgs/entitlements?ownerId={organizationId} returns the legacy entitlement grants for one organization. The caller must have OrganizationEntitlementsView in that exact organization. Ordinary membership, legacy roles, and access to a shared cluster do not grant entitlement visibility.

GET /v1/users/{userId}/entitlements is limited to the current user's own legacy global grants. Sharing an organization with another user—including as that organization's Owner—does not permit reading the other user's global entitlements. Platform operators use the separately authorized Admin surface.

Add An Existing User

Call POST /v1/orgs/{organizationId}/members with an existing user ID. The smallest request creates only an organization membership shell:

{
  "userId": "00000000-0000-0000-0000-000000000000",
  "groupIds": [],
  "directAssignments": []
}

The shell identifies organization membership but grants no protected product access by itself. You can atomically enroll the new member into permission groups and add direct permissions:

{
  "userId": "00000000-0000-0000-0000-000000000000",
  "groupIds": [
    "11111111-1111-1111-1111-111111111111"
  ],
  "directAssignments": [
    {
      "capabilityKey": "LocationDocuments",
      "scopeType": "Location",
      "scopeId": "22222222-2222-2222-2222-222222222222",
      "parentClusterId": "33333333-3333-3333-3333-333333333333",
      "value": "Read"
    }
  ]
}

The caller must hold invitation authority plus the required group-management, permission-management, delegation, resource, and administrative authority for the complete payload. If any requested grant is not authorized, nothing is created. A successful request returns 204 No Content.

The legacy role input no longer establishes access precedence. Omit it; old clients may send only "role": "User" during the compatibility period.

Remove A Member

Call DELETE /v1/orgs/{organizationId}/members/{userId}. The caller must have OrganizationMembersRemove; legacy role precedence does not authorize or block removal. Protected organization owners cannot be removed through this endpoint; use the dedicated owner lifecycle. The combined roster exposes the authoritative protected relation through isOwner and sets actions.canRemoveMember to false for those rows. Do not infer ownership from the compatibility role value. The roster's top-level authorizationStateConcurrencyVersion supplies the optimistic version for protected-owner preview and apply requests without requiring permission-group visibility.

Invite A User

Create an email invitation with POST /v1/orgs/{organizationId}/invitations. Provide exactly one target—an email address or existing user ID—an idempotency key, and the intended permission groups:

{
  "email": "new.member@example.com",
  "groupIds": [
    "11111111-1111-1111-1111-111111111111"
  ],
  "directAssignments": [],
  "idempotencyKey": "invite-new-member-2026-07-27"
}

The invitation destination and expiry are server-controlled. Clients cannot provide a website URL, and responses never contain the token or complete invitation link. A successful response means the invitation and its email delivery work were committed; delivery remains asynchronous and its current state is visible in the returned roster entry.

The optional directAssignments field uses the same shape and authorization rules as the add-member request. Omit both access arrays for a zero-permission membership shell.

Creating the invitation requires OrganizationMembersInvite, plus the independent group, assignment, resource, and delegation capabilities needed by the complete requested payload.

An active invitation is shown in the organization roster as a pending invitation—a ghost row with no user behind it. It is not a user, membership, permission principal, effective grant, reserved seat, or billable member. Organization member capacity is checked only when acceptance would create the real membership. isOwner: true on a pending row means protected-owner intent, not active ownership; it grants no authority and does not count as an organization owner before acceptance.

Edit or cancel a pending invitation

Replace the complete intended group set with PUT /v1/orgs/{organizationId}/invitations/{invitationId}/groups. Send the latest invitation and organization authorization-state concurrency versions, a new idempotency key, and the full desired groupIds array. An empty array keeps the invitation active but makes it a shell-only invitation. Editing groups creates an immutable revision; the original link, target, creation time, and expiry remain unchanged. An ordinary group revision preserves any current protected-owner intent and cannot add or remove it.

A current protected owner can add or remove pending owner intent through POST /v1/orgs/{organizationId}/owners/invitations/{invitationId}/validate and the matching apply route without /validate. Both requests require a recent interactive login and this body:

{
  "isOwner": true,
  "expectedInvitationConcurrencyVersion": 4,
  "expectedAuthorizationStateConcurrencyVersion": 12,
  "idempotencyKey": "pending-owner-2026-07-29"
}

Use actions.canAddOwner and actions.canRemoveOwner on the ghost row to present the protected Eigentümer system chip beside ordinary groups. Reload the roster after apply. Removing the chip changes only pending intent; it never removes an existing protected owner.

Use the action flags on the roster row to present these controls:

  • DELETE .../invitations/{invitationId} revokes the invitation.
  • POST .../invitations/{invitationId}/delivery/retry retries a failed email while its protected delivery payload is still available.
  • POST .../invitations/{invitationId}/reissue rotates the token, invalidates the old link, and queues a new email without changing the current groups.

Every mutation sends a stable idempotency key and uses the row's latest concurrencyVersion. Do not generate a new idempotency key merely because a network response was lost. On invitation_stale_version or another retryable conflict, reload the authoritative roster row before offering the mutation again.

Invitation group revisions are authorized when committed. Later loss of the authorizing manager's access does not invalidate a safe committed revision. Acceptance still revalidates the bearer token, expiry, revocation, referenced resources, current entitlement ceilings, owner safeguards, current group permission versions, and administrative ceilings atomically. The delivery address identifies where the link was sent; it does not restrict who may use the link. A broadened administrative group or narrowed entitlement ceiling returns a retryable conflict without consuming the invitation.

Preview, register, or log in

The emailed link carries the token only in its URL fragment. The invitation page removes that fragment immediately and keeps the token in per-tab session storage while the flow is active. It calls POST /v1/orgs/invitations/preview to show only the organization display name, masked target email, current group names, expiry, and bounded availability. Preview never reveals whether an account already exists.

Existing authenticated users accept with POST /v1/orgs/invitations/accept. New users register and accept with the anonymous POST /v1/orgs/invitations/register route. Registration asks for username, email, and password. The account email may differ from the address that received the invitation. Account, membership, current intended groups, direct assignments, any current protected-owner intent, invitation consumption, audit, and derived work commit atomically, and the response creates the normal browser session without a second login. Because the link is a bearer capability, the active account that accepts a link carrying owner intent becomes the protected owner even when its email differs from the delivery address.

The invitation page also offers switch to login. Login and MFA complete on the same page without losing the per-tab invitation state. A signed-in user must explicitly confirm acceptance and can switch accounts before accepting. Any active account holding the link may accept it. If that account is already an organization member, acceptance preserves all existing group memberships and adds only the invitation's missing groups. Successful acceptance consumes the invitation, so the pending row for the delivery address disappears. The response indicates whether the user was already a member so clients can refresh organization state without creating duplicate membership UI. Accepted membership uses the User compatibility role as a shell value; access comes from protected ownership, permission groups, and direct assignments.

Invitation links are single-use bearer credentials. Anyone who obtains a valid link can accept it with their active account or use it to create an account. Recipients should not forward the link; administrators should revoke or reissue it if disclosure is suspected.

Unavailable, accepting-user-invalid, account-exists, stale-version, group-authority-changed, tier-ceiling-changed, capacity, terminal-conflict, and delivery-not-retryable results include a stable code and retryable flag. Clients should preserve per-tab state for retryable errors and clear it after success or terminal unavailability.

Read and write branding and usage

Organization branding reads, including asset metadata, downloads, and previews, require OrganizationBrandingRead. Metadata changes, upload start/completion, and asset deletion require OrganizationBrandingWrite. Neither operation uses Owner/Admin role precedence.

The authoritative capacity summary at GET /v1/orgs/{organizationId}/usage requires OrganizationUsageView. Membership by itself exposes no storage, current-resource, monthly-Job, lifetime-Job, or remaining-capacity information. The former owner-quota storage routes are hidden compatibility stubs and return HTTP 410.

On this page