> ## Documentation Index
> Fetch the complete documentation index at: https://docs.gumnut.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Library Sharing

> Library roles, invitation and membership contracts, and credential scope

When someone shares a library with you, you can access it alongside libraries
you own. Each library has one owner. Its `LibraryResponse` includes `owner`
(an ID and optional display name) and your `role` (`owner`, `viewer`, or
`collaborator`). The existing `user_id` remains the owner's ID. Your role
describes membership; your credential can further limit your access.

## Role permissions

Your active library role sets these content and management limits:

| Operation                                                 | Viewer | Collaborator | Owner |
| --------------------------------------------------------- | ------ | ------------ | ----- |
| Browse, search, download, and read content or trash       | Yes    | Yes          | Yes   |
| Upload and edit photos or metadata                        | No     | Yes          | Yes   |
| Add, replace, delete, or revert photo versions            | No     | Yes          | Yes   |
| Create, edit, or delete albums; organize faces and people | No     | Yes          | Yes   |
| Trash or restore photos                                   | No     | Yes          | Yes   |
| Permanently delete photos or empty photo trash            | No     | No           | Yes   |
| Rename, trash, restore, or delete the library             | No     | No           | Yes   |
| Manage invitations or members                             | No     | No           | Yes   |
| Leave the library                                         | Yes    | Yes          | No    |

Deleting an album, face, or person is an organizational edit a collaborator can
make; permanently deleting photos or a library is owner-only. Your credential
must also allow the action and library. For example, deleting or reverting a
photo version still requires the `delete_permanently` credential action, even
when your role allows the operation. Invitation and membership operations also
require a first-party session, as described below.

## Credentials and scope

Content operations apply your current library role **and** your credential's
action and library scope. With an `all_libraries` API key or OAuth grant, you
can reach every library currently accessible to your account, including ones
you join after creating the credential. This also expands older credentials
that previously reached only libraries you owned. A `selected_libraries`
credential stays fixed to its selected IDs; joining a library does not add it
to that set. You lose access if your membership is revoked or the owner's
account or library becomes unavailable.

Review your existing `all_libraries` integrations when joining a library. Use a
selected-library credential when an integration should keep a fixed scope.
You still need to be the owner for owner-only actions.

<Warning>
  You need a **first-party Gumnut session** to create, list, retrieve, or disable
  invitations; preview or join an invitation; manage members; or leave a library.
  You cannot call these operations with an API key or delegated OAuth credential.
  The generated Python and TypeScript SDK methods describe the HTTP contract,
  but configuring an SDK client with an API key does not grant you access.
</Warning>

## Selecting a library

`GET /api/libraries` returns your owned and joined libraries as an unpaginated
array. Each row identifies the owner and your current role without disclosing
the owner's private account usage. Once you have more than one live library,
content requests that omit `library_id` fail with `400` and
`library_id required because user has multiple libraries`. Pass an explicit
library ID in SDK, MCP, and other multi-library clients. Joining a library
does not automatically select it on your device.

## Membership and invitations

As an owner, manage memberships at `/api/libraries/{library_id}/members`.
`GET` lists non-owner memberships, with optional `state`, `limit`, and
`starting_after_id`. Use `PATCH` on `/{user_id}` to change an active member's
role or `DELETE` to remove them. If you are a member, leave through
`POST /api/libraries/{library_id}/leave`. The library response represents the
owner without a synthetic membership row. Membership responses include the
member's public summary, role, state (`active`, `left`, or `removed`), and
timestamps. If you remove someone, an older invitation cannot readmit them;
leaving does not impose that cutoff.

Invitation operations live under `/api/libraries/invitations`, **not** under
`/api/invitations` or an individual library's path. Each v1 invitation targets
one library. As the owner, create one by posting `library_id`, `role` (`viewer`
or `collaborator`), an absolute `expires_at`, and `max_joiners`. If you post an
identical active policy, you get its existing link (`200`); a new policy returns
`201`. A matching inactive policy returns `409 invitation_inactive` without a
link. You can list invitations across your owned libraries or filter with
`library_id`; the list contains metadata and usage, never secrets. Use
`GET /{invitation_id}/link` to retrieve an active link, or
`POST /{invitation_id}/disable` to stop new admissions without removing
members.

<Note>
  You may be unable to issue invitations or join while sharing admission is
  disabled for rollout. A route appearing in the API reference or an SDK does
  not mean admission is enabled for your account.
</Note>

Your browser link has the form `/invite/{invitation_id}#secret=<secret>` on the
Gumnut app origin. Send its secret to the API only in the JSON body of
`POST /api/libraries/invitations/{invitation_id}/preview` or `/join`; keep the
link and secret out of logs and analytics. Preview returns limited library and
owner attribution plus your eligibility without reserving a place. Join
rechecks eligibility and commits your membership. Repeating a successful join
does not consume another place or replace your existing role. Link retrieval
and creation responses contain the secret-bearing URL; listing, preview, join,
and ordinary library responses do not.

Member and invitation lists return `{ data: [...], has_more: boolean }` in
newest-first order. `limit` defaults to 20 and accepts 1–200. Pass the last
row's **membership ID** or **invitation ID** as `starting_after_id` to get the
next page; do not use a member's user ID as a membership cursor.

Sharing domain errors use `{ "detail": "...", "code": "..." }`.
For example, join can return `409 invitation_expired`,
`invitation_disabled`, `invitation_exhausted`, or
`new_invitation_required`. Preview and join return the same generic
`404 invitation_unavailable` for invalid secrets, missing invitations, and
unavailable libraries. Authentication, permission, rate-limit, and request
validation failures retain their usual `401`, `403`, `429`, and `422` shapes.

## Storage and contribution

When you upload photos to a joined library, they count against its owner and
library cap rather than your own allowance. A capacity failure does not reveal
the owner's private account usage. Newly stored photos record you as the
contributor independently of the owner. A duplicate upload keeps the original
photo's contributor; historical attribution can be unknown. Your
contributions remain in the library after you leave or are removed.
