# Publication invitations, applications and notifications

Blog authorizes callers and applies accepted requests to the publication’s pinned
Management group. Relationship Service owns the request lifecycle and recipient
mailbox. Relationship state alone never authorizes a Blog content or management
operation.

All routes below use POST, the normal Blog response envelope and bearer authentication and an active
Blog roster principal. The selected organization is resolved through Blog’s live,
recipient-authenticated organization check. Omitting organization selection acts
as the authenticated personal user or machine app. Clients may send
`workspace_principal_id` to reject an action if the resolved workspace changed.

## Routes

All paths below begin with `/v1/`. UUID fields identify the selected publication,
Blog roster principal, or enrollment command. Decision `version` is the integer
returned by the current status response.

| Route | Required input | Optional input / result |
| --- | --- | --- |
| `/v1/member/invite/create` | `publication_id`, recipient `principal_id`, `idempotency_key` | `message`; returns request identifiers and status. |
| `/v1/member/apply/create` | `publication_id`, `idempotency_key` | `message`; applicant is the verified current workspace. |
| `/v1/member/invite/status`, `/v1/member/apply/status` | `command_id` | Returns request, `can_respond`, `grant_applied`, and `access_active`. |
| `/v1/member/invite/accept`, `/v1/member/invite/decline`, `/v1/member/invite/cancel` | `command_id`, `version` | Applies the permitted recipient or manager decision. |
| `/v1/member/apply/accept`, `/v1/member/apply/decline`, `/v1/member/apply/cancel` | `command_id`, `version` | Applies the permitted manager or applicant decision. |
| `/v1/member/requests/list` | `publication_id` | `count` (1–50, default 25), `offset`; paged `items`, nullable `next_offset`. |
| `/v1/notification/list` | None | Same `count` and `offset`; current workspace's mailbox. |
| `/v1/notification/read` | `notification_id` | Idempotent delivery acknowledgement. |

All routes accept optional `workspace_principal_id` to check that the current
workspace still matches the client's displayed selection. Organization
selection follows [workspaces](https://m7.org/docs/api/api.blog.service.m7.org/workspaces.md).

## Invitations

A current publication Owner or Admin with `member.save` sends
`/v1/member/invite/create` with `publication_id`, the recipient’s Blog `principal_id`,
a fresh UUID `idempotency_key`, and an optional `message` (up to 2,000 UTF-8 bytes).
The recipient must already be active in the Blog roster. Enrollment grants
ordinary Member access; role assignment is a separate management action.

The response contains `request.command_id`, `request.request_id`, `state`,
`version`, the publication/recipient/issuer labels, message and expiry. Reuse the
same idempotency key and input after a timeout. Changing input under that key
returns a conflict. A fresh request uses a fresh key.

`/v1/member/invite/status` takes `command_id`. Only its recipient workspace or an
authorized publication manager may inspect it. The response includes
`can_respond`, `grant_applied`, and `access_active`.

The recipient sends `/v1/member/invite/accept` or `/v1/member/invite/decline` with
`command_id` and the integer `version` displayed by status. Organization
responses require a currently verified organization Owner or Admin whose
organization profile includes `member.save`. Membership is granted to the
organization principal, while the actual actor is recorded separately.

An authorized publication manager can send `/v1/member/invite/cancel` using the same
command/version fields. Invitations expire after seven days. Expired or cancelled
requests cannot grant access. The issuer must still actively manage the
publication when an uncompleted invitation grant is applied.

## Applications

An authenticated user or machine app sends `/v1/member/apply/create` with
`publication_id`, a fresh UUID `idempotency_key`, and optional `message`.
`principal_id` is not accepted: the applicant is the verified current workspace.
Organization applicants require the same organization authority as invitation
responses. Existing membership must be managed before requesting enrollment.

`/v1/member/apply/status` takes `command_id`. The applicant can withdraw with
`/v1/member/apply/cancel`. A current publication Owner or Admin with `member.save`
approves or declines through `/v1/member/apply/accept` or `/v1/member/apply/decline`, using
`command_id` and `version`. An applicant cannot approve their own application
without current publication management authority.

Managers load the Access queue through `/v1/member/requests/list` with
`publication_id`, optional `count` (1–50, default 25), and `offset`. Its `items`
contain pending invitations/applications and, on the first page, accepted grants
that require recovery. Continue with `next_offset` until it is null.

## Notifications

`/v1/notification/list` takes optional `count` and `offset`. It returns the current
`principal_id`, `workspace_name`, `items` and `next_offset`. Each item contains
`notification_id`, `type`, `action`, `created_at`, `delivered_at`, and optionally a
publication label. An invitation includes `command_id` for its separate status
and consent dialog. Mailboxes are scoped to the verified Personal or organization
principal and span publications.

After displaying an entry to its recipient, send `/v1/notification/read` with
`notification_id`. This records an idempotent delivery acknowledgement. Listing
and previewing do not acknowledge entries. Delivery acknowledgement is separate
from accepting or declining a request and never grants membership. Acknowledged
entries remain in the notification list.

## Removal and leaving

`/v1/member/remove` retains its existing `publication_id`, `principal_id` and exact
`member_id` contract; `/v1/member/leave` accepts the current workspace’s
`publication_id`. Both recheck current Blog authority and the last-owner rule before removing the matching active Relationship
bond in the protected publication scope, then delete the Blog membership.

Relationship records manager removal as `kick` (or `system_kick`) and the
recipient’s own departure as `voluntary_leave`. The notification projection
includes `removal_type`, and the mailbox displays “You were removed from the
publication” or “You left the publication” with the publication name. Removing a
legacy direct membership with no Relationship bond still removes it locally;
there is no Relationship removal event for a bond that never existed.

Remote failure returns an error instead of reporting successful removal. If
Relationship committed but its reply was lost, or the subsequent Blog membership write
failed, retry the same Remove/Leave: Blog finishes the local deletion after seeing
that the remote bond is already absent. The existing remote event is retained,
without a duplicate removal notification. A completed grant receipt is retained
so replaying an old acceptance never restores removed access.

## Recovery and access authority

Remote acceptance and the Blog membership grant can complete separately. If
acceptance succeeds while the membership write fails, status shows accepted with
`grant_applied=false`; retry Accept to finish the grant. The Access queue also
shows these incomplete grants to managers.

Once status reports a successfully applied membership grant, replaying Accept
never creates or reactivates a membership. Removing, disabling or leaving a
membership continues to take effect through Blog Management, regardless of the
historical request or Relationship event. Existing active memberships keep their
roles and capabilities. Inactive memberships require an administrator to resolve
them before enrollment can continue.

The browser closes stale invitation details on workspace changes and discards
responses belonging to the previous workspace. Backend authority is independently
checked on every action. Use the verified current workspace for every action.

An unavailable Relationship service returns an error for invitation,
application, or mailbox actions; an empty result is not a successful fallback.
It does not remove otherwise valid publication content access. Reload the
request status after recovery and keep the original idempotency key for a
timed-out creation. A publication with no enrollment history has an empty
Access queue. Existing direct membership management remains available under
its documented Blog authority; do not interpret a Relationship outage as a
reason to grant access or auto-accept a request.
