> For AI agents: the complete documentation index is available at /llms.txt, the full documentation bundle is available at /llms-full.txt.

# Project members and invitations

The default Go API serves these reads through the Project owner. The
[development fixture mode](/reference/contracts/project/http_api/member-invitation-preview/README.md#clients-errors-and-rollout)
remains available through explicit configuration. Generated clients and
disposable fixture checks do not establish support in a deployed backend.

Both reads require a current Party identity and a canonical Project UUID.
Browsers use the Party session; the enrolled MCP gateway can use its
[operation-bound assertion](/reference/contracts/mcp/README.md#authority) under the same current
owner permission checks.
Responses use `Cache-Control: private, no-store`. Neither operation accepts query
parameters. These reads add no mutation, realtime subscription or automatic polling.

## Members

`GET /api/v1/projects/{projectId}/members`, operation `getProjectMembers`, requires
current `project.view` authority. The backend checks Project/source authority
before and after the read.

```json
{
  "project_id": "11111111-1111-4111-8111-111111111111",
  "revision": 12,
  "items": [
    {
      "party_id": "22222222-2222-4222-8222-222222222222",
      "roles": ["project-administrator"],
      "asserted_name": "Alex Morgan",
      "asserted_name_revision": 7
    },
    {
      "party_id": "33333333-3333-4333-8333-333333333333",
      "roles": ["developer"],
      "asserted_name": null,
      "asserted_name_revision": null
    }
  ],
  "pending_member_count": 1,
  "membership_complete": false
}
```

All properties are required. `asserted_name` and `asserted_name_revision` are
nullable. The name is the existing trusted AAI identity projection's name;
editable profile names and email addresses are never used as fallbacks.
A current projection with no name has a null name and its non-null revision.
A pending, superseded or currently ineligible identity projection has both fields
null. A missing name preserves the membership row and roles. Infrastructure or
corrupt-authority failures fail the whole query.

`revision` versions Project membership. `asserted_name_revision` independently
versions the Party identity projection and must not be compared between Parties.
An unchanged Project revision does not imply unchanged names.

`items` is ordered by `party_id`. The complete source observation contains at
most 256 member bindings; several qualified bindings can contribute to one Party
row. Every observed binding is resolved into a row or counted in
`pending_member_count`. `membership_complete` equals `pending_member_count === 0`.
This flag concerns membership resolution; it says nothing about missing names.
The count is diagnostic information and must never be labelled as invitations.
Show available rows and a simple incomplete-membership state when the flag is
false. Use a neutral missing-name label or shortened canonical Party ID.

The retained Project page uses `asserted_name` for every member, including the
viewer. Public profile visibility does not affect this authorised list. Its
own-profile read identifies the `You` marker only. The browser regression runs
with private/absent public profiles, independent name revisions, unavailable
reads and a delayed response from a previously selected Project:

```sh
pnpm exec playwright test --config acceptance/browser/project-members.config.ts
```

## Invitations

`GET /api/v1/projects/{projectId}/invitations`, operation `listProjectInvitations`,
requires current `project.access.manage` authority, currently granted to Project
administrators. Show this separate section only to those administrators.

```json
{
  "project_id": "11111111-1111-4111-8111-111111111111",
  "project_revision": 12,
  "read_at": "2026-11-06T12:00:00Z",
  "items": [
    {
      "invitation_id": "44444444-4444-4444-8444-444444444444",
      "recipient_email": "new.member@example.invalid",
      "roles": ["developer"],
      "state": "pending",
      "expires_at": "2026-11-20T12:00:00Z"
    }
  ]
}
```

All properties are required and non-null. `invitation_id` is an opaque reference,
not a canonical Party ID or acceptance credential. The backend derives this scoped reference without exposing the provider invitation locator. `recipient_email` is available
only through this administrator-authorised read and is not an asserted name.
Roles use the same canonical values as members.

This slice returns only provider-pending invitations whose expiry is after
`read_at`. It excludes accepted, cancelled, rejected and expired invitations.
There is no historical invitation list or acceptance/revocation operation.
`expires_at` is the provider's actual expiry, not a locally invented lifetime.

The backend resolves the exact canonical Project/provider binding, traverses all
provider pages and checks Project scope and customer itself. It rechecks current
administrator authority after acquisition. `items` contains the complete result,
bounded to 256 invitations and sorted by `expires_at`, then `invitation_id`.
There is no cursor. Overflow, missing pages, unsupported roles and malformed
provider data fail the whole request; partial results are never returned.

`project_revision` versions the Project authority used for this read, not the
invitation collection. `read_at` is successful acquisition completion time.
The provider supplies no atomic collection revision; this timestamp does not
establish a transactional snapshot across provider pages.

## Clients, errors and rollout

The source schemas are <a href="/source/contracts/project/http_api/members.paths.json.txt">members.paths.json</a>,
<a href="/source/contracts/project/http_api/invitations.paths.json.txt">invitations.paths.json</a> and <a href="/source/contracts/project/http_api/schemas.json.txt">schemas.json</a>.
Regenerate with `pnpm generate:contracts`. Both generated clients export:

```ts
import {
  getProjectMembers,
  listProjectInvitations,
} from "@/adaptors/generated/http/project/project";

const members = (await getProjectMembers(projectId)).data;
const invitations = (await listProjectInvitations(projectId)).data;
```

The import is relative to the active frontend's `src` alias. Sources are
`services/web/src/adaptors/generated/http/project/project.ts` and
`compat/themelio/services/web/src/adaptors/generated/http/project/project.ts`.
The canonical web client also exports `useGetProjectMembers` and
`useListProjectInvitations`. Query-cache values must be cleared when the Party
session or Project access changes; HTTP `no-store` is not a React Query cache policy.

| Status | Meaning                                                                                                | Frontend handling                                                                                                           |
| ------ | ------------------------------------------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------- |
| `200`  | Successful bounded read                                                                                | Empty invitations prove an empty invitation list; an empty roster proves emptiness only when `membership_complete` is true. |
| `400`  | Invalid Project ID or any query parameters                                                             | Fix the request.                                                                                                            |
| `401`  | Missing or no longer current Party session                                                             | Follow the existing sign-in/session flow.                                                                                   |
| `404`  | Missing or inaccessible Project for this operation                                                     | Show unavailable/access state; do not reveal whether the Project exists.                                                    |
| `503`  | Stale/changing authority, unavailable dependency, incomplete/oversized acquisition or invalid response | Show unavailable/retry state, retaining no claim that the list is empty.                                                    |

The generated fetch adaptor rejects non-success responses with the existing
`HTTPError`; inspect its status. Error bodies contain `code`: `invalid_input`
for 400, `unauthorised` for 401, `not_found` for 404 and
`temporarily_unavailable` for 503. Keep the two sections' loading/error states
independent. An invitation read failure does not invalidate a successful member
read.

Reconcile the API-auth credentials and deploy the Waldur actor capability before
the new Project owner, then deploy the new API and enable frontend use of the
new fields. The <a href="/source/services/beto/contexts/project/adaptors/waldur/README.md.txt">invitation adaptor guide</a>
describes the scoped credential, URL and trust requirements. The established owner member query retains its original wire shape
for existing API instances. The new API calls `GetProjectMemberDetails`; an
older owner cannot satisfy that query and the read fails unavailable. Its reply
preserves presence of `membership_complete`, and the API rejects absent presence.
For real data, leave `API_PROJECT_READ_FIXTURES` unset or `false` and remove both
fixture scenario settings. An unavailable owner or provider produces an error;
the API never substitutes fixture data. Clear any cached fixture responses when
switching the development frontend to real reads.
Deploy backend support before relying on the new required fields. During a
mixed-version rollout, absent fields are unsupported-contract evidence, not
equivalent to null, `false` or zero. An old backend's invitation-route `404` must
not be converted into an empty invitation list. Frontend fixture work can proceed
against <a href="/source/contracts/project/http_api/fixtures/members.json.txt">fixtures/members.json</a>,
<a href="/source/contracts/project/http_api/fixtures/invitations.json.txt">fixtures/invitations.json</a> and
<a href="/source/contracts/project/http_api/fixtures/invitations-empty.json.txt">fixtures/invitations-empty.json</a>; these are
contract examples, not live deployment evidence.

<details class="page-source"><summary>Page source</summary>

<a href="/source/contracts/project/http_api/README.md.txt">Source snapshot</a> · Revision `f928834c8`.

</details>
