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:
idusernameisKnown: falseprofilePicturewhenincludeProfilePicture=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/retryretries a failed email while its protected delivery payload is still available.POST .../invitations/{invitationId}/reissuerotates 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.