# Project members and capabilities

Project membership controls access to that project's mail and logs. Use
canonical principal UUIDs, rather than usernames, client IDs, or native
organization IDs. Obtain membership IDs from the current project-member
list. Every request uses the selected [workspace](https://m7.org/docs/api/api.mail.m7.org/workspaces.md), authenticated
`POST` JSON, and a `project_id`.

## Read members

`POST /projects/members/list` requires `project.member.list` and accepts
`project_id`, optional `limit` (integer 1–100, default 50), and optional
`offset` (integer 0–1,000,000, default 0).

```json
{"project_id":"<project-uuid>","limit":50,"offset":0}
```

The response `data` contains `project_id`, `group`, `caller_role`,
`available_roles`, `active_owner_count`, `roles`, `members`, `total_count`,
`limit`, and nullable `next_offset`. Continue at `next_offset` until it is
`null`. The group describes the pinned Management group.

Each member includes `member_id`, `principal_id`, `role`, `status`, timestamps,
and a nullable `principal` display summary (`name`, `kind`, `status`). The
response also supplies `is_self`, `final_owner`, `can_leave`, `can_update`,
`can_remove`, `allowed_roles`, `allowed_statuses`, `can_edit_capabilities`,
`capabilities_mode`, and `capability_tag_ids`. Use these current controls
instead of deriving buttons from display labels. `service_access` describes
the target's elevated service role; it does not grant project authority.

## Add, save, remove, and leave

| Route | Fields after `project_id` | Required action |
| --- | --- | --- |
| `/projects/members/add` | Required `principal_id` | `project.member.add` |
| `/projects/members/save` | Required `principal_id`; optional `member_id`, `role`, `status` | `project.member.save` |
| `/projects/members/remove` | Required `principal_id` and `member_id` | `project.member.remove` |
| `/projects/members/leave` | No other identity or membership fields | `project.member.leave` |

Add creates an ordinary active Member for a target that already has active
Mailbox service access. It accepts no caller-supplied role or status. It
returns the membership's `id`, `principal`, `role`, and `status`. An existing
membership causes `409`; edit it explicitly.

Save creates a membership when `member_id` is omitted; a new membership
defaults to `role: "member"`, `status: "active"`. To edit an existing
membership, send its exact ID. Omitted role/status preserve existing state.
Allowed role values are `owner`, `admin`, and `member`; editable statuses are
`active` and `disabled`. New or activated targets must have active service
access. Existing membership identity and principal must match. Changing a
role clears prior custom capability selections and restores role defaults.

```json
{
  "project_id":"<project-uuid>","principal_id":"<principal-uuid>",
  "member_id":"<membership-uuid>","status":"disabled"
}
```

Owners can manage all project roles. Admins can create or manage only Members;
they cannot promote, disable, or remove an Owner/Admin. Role changes and
privileged-target changes also require `project.member.assign_roles`.
Removing, disabling, demoting, or leaving as the last active Management
Owner fails with `409`. Add another active Owner before transferring that
responsibility.

Save returns `saved: true`; remove returns `removed: true`. Both include
`project_id`, a `member` result with membership/principal identity, and
`context`. Save's member result also has `role` and `status`. `context` may be
`null` after a successful self-demotion or removal revokes subsequent access.
Removal deletes the membership and its capability assignments. Leave removes
only the current effective principal's membership and returns
`{"left":true,"project_id":"<project-uuid>"}`. It rejects identity,
membership, role, or status fields for another target.

## Read and save capability selections

`POST /projects/members/capabilities/list` accepts only `project_id` and
requires `project.member.list`. It returns `project_id`, `read_only: true`,
`tags`, and `roles`. Each tag has `tag_id`, exact `capability`, `name`, and
`description`. Each role describes `default_tag_ids`, `allowed_tag_ids`,
`protected_capabilities`, and catalogue `version` with configuration metadata.

`POST /projects/members/capabilities/save` requires an Owner/Admin with
`project.member.save` and accepts exactly:

```json
{
  "project_id":"<project-uuid>","principal_id":"<principal-uuid>",
  "member_id":"<membership-uuid>","mode":"custom","tag_ids":[]
}
```

`mode` is `defaults` or `custom`. Both require `tag_ids` as a JSON array.
For `defaults`, supply an empty array to use current role defaults. For
`custom`, select distinct returned tag UUIDs allowed for that target role;
an empty selection grants no optional actions. Protected membership rights
remain. Organization grants can further restrict all effective rights.
Admins can edit only Member capability selections.

Optional project actions include `mail.send`, `mail.log.search`, and
`mail.log.view`; Members may also receive `project.member.list`. Discover
current defaults and selections rather than hardcoding them. Save returns
`saved: true`, `project_id`, and the caller's refreshed `context`.

`GET /capabilities.json` returns the service's capability document without
the business-response envelope. It includes `schema_version`,
`catalogue_version`, `families`, `capabilities`, `presets`, and
`role_defaults`, with descriptive metadata. No access token is required.
The document describes possible actions; it does not grant them or identify
the current caller's effective permissions. Unavailable catalogue data can
return `503` in the ordinary failure envelope.

[Return to Mailbox](https://m7.org/docs/api/api.mail.m7.org/README.md).
