# Sharing and orgs (/docs/sessions/sharing-and-orgs)

Grant a role on a session to a person, an organisation or a link; list what is shared with you; manage org membership.

A **share** grants a `role` on **one** session to a `user`, to every member of an `org`, or to whoever presents a `link` token. The session owner manages shares; anyone can create an org and invite people into it.

| `principal_type` | `principal_id` | Who gets access |
| --- | --- | --- |
| `user` | The user's UUID | That account, on every device. |
| `org` | The org's UUID | Every current member of the org. Only an org member can share a session into it. |
| `link` | — (must be omitted) | Any authenticated principal who presents the token. |

`role` is `viewer` (default), `editor` or `owner`. `expires_at` is optional and must be in the future; an expired share is ignored everywhere. Sharing with the session's own owner is `400` — *The owner already has full access.*

## Share a session [#share-a-session]

<Tabs items={['With a person', 'With an org', 'By link']}>
<Tab value="With a person">

```bash
curl -X POST https://ai.codai.ro/v1/sessions/5b3e…/shares \
  -H "Authorization: Bearer $CODAI_API_KEY" -H "Content-Type: application/json" \
  -d '{ "principal_type": "user", "principal_id": "7d21…-uuid", "role": "editor" }'
```

```json
{
  "id": "e4a9…", "session_id": "5b3e…", "principal_type": "user", "principal_id": "7d21…",
  "role": "editor", "has_token": false, "expires_at": null,
  "created_by_user_id": "…", "created_at": "2026-09-23T10:00:00.000Z"
}
```

Unknown user → `404 not_found` (*User not found.*). A non-UUID `principal_id` → `400` (*principal_id must be a user uuid.*).

</Tab>
<Tab value="With an org">

```bash
curl -X POST https://ai.codai.ro/v1/sessions/5b3e…/shares \
  -H "Authorization: Bearer $CODAI_API_KEY" -H "Content-Type: application/json" \
  -d '{ "principal_type": "org", "principal_id": "0c4f…-uuid", "role": "viewer", "expires_at": "2026-10-01T00:00:00+00:00" }'
```

You must be a member of that org — otherwise `403 forbidden` (*You are not a member of this org.*). Unknown org → `404` (*Org not found.*).

</Tab>
<Tab value="By link">

```bash
curl -X POST https://ai.codai.ro/v1/sessions/5b3e…/shares \
  -H "Authorization: Bearer $CODAI_API_KEY" -H "Content-Type: application/json" \
  -d '{ "principal_type": "link", "role": "viewer", "expires_at": "2026-09-30T00:00:00+00:00" }'
```

```json
{
  "id": "b12c…", "session_id": "5b3e…", "principal_type": "link", "principal_id": null,
  "role": "viewer", "has_token": true, "expires_at": "2026-09-30T00:00:00.000Z",
  "created_by_user_id": "…", "created_at": "2026-09-23T10:00:00.000Z",
  "token": "Qm9…base64url…"
}
```

The `token` is 32 random bytes, base64url, returned **exactly once** in this `201`. The server keeps only its SHA-256, so it cannot be shown again — store it or create another share. Present it as `x-codai-share-token` on any request, or `?share=<token>` on SSE/WS.

</Tab>
</Tabs>

<Callout type="warn">
A link token grants the share's role to **any authenticated codai user** who presents it. Treat it like a password: set `expires_at`, keep the role at `viewer` unless you need more, and delete the share when done.
</Callout>

## Manage shares [#manage-shares]

| Method | Path | Min role | Response |
| --- | --- | --- | --- |
| `GET` | `/v1/sessions/:id/shares` | owner | `{ shares: Share[] }` — never includes tokens, only `has_token`. |
| `POST` | `/v1/sessions/:id/shares` | owner | `201` Share (+ `token` once for `link`). |
| `DELETE` | `/v1/sessions/:id/shares/:shareId` | owner | `{ deleted: true, id }`; unknown → `404` (*Share not found.*). |

Deleting a share revokes access immediately for everyone it covered — including anyone currently connected via a link token, whose next request is `403 not_a_member`.

## What is shared with me [#what-is-shared-with-me]

```bash
curl "https://ai.codai.ro/v1/sessions/shared-with-me" -H "Authorization: Bearer $CODAI_API_KEY"
```

```json
{ "sessions": [ { "id": "5b3e…", "session_key": "…", "owner_user_id": "…", "title": "Fix the flaky test", "created_at": "…", "last_event_at": "…", "last_seq": 42, "executor_device_id": "1c2f…", "lease_expires_at": "…", "e2e": false, "archived": false, "role": "editor", "share_id": "e4a9…" } ] }
```

Lists sessions shared with you directly or through an org (up to 200; `?archived=1` includes archived ones). Link-share access is exercised per request and **does not appear** in any list — a viewer who arrived by link has to keep the token. `GET /v1/sessions` (with `x-codai-device` or `?v=2`) returns your own sessions first, then the shared ones, each with your `role`.

Your effective role on a session is the **highest** across every share that matches you — a `viewer` link plus an `editor` user share makes you an editor.

## Organisations [#organisations]

An org is a named group of users. Sharing a session with an org shares it with everyone in it, now and later.

<Callout type="info">
On the wire the Org object is **camelCase** — `{ id, name, ownerUserId, createdAt }` — unlike every other object on this surface. This is part of v1 and will not be renamed.
</Callout>

| Method | Path | Who | Request | Response |
| --- | --- | --- | --- | --- |
| `POST` | `/v1/orgs` | any user | `{ "name": string 1..200 }` | `201 { id, name, ownerUserId, createdAt, role: "owner" }` |
| `GET` | `/v1/orgs` | any user | — | `{ orgs: [{ id, name, ownerUserId, createdAt, role }] }` — orgs you belong to, with your role |
| `GET` | `/v1/orgs/:id/members` | org member | — | `{ org_id, members: [{ user_id, role, created_at, email }] }` |
| `POST` | `/v1/orgs/:id/members` | org owner or admin | `{ "user_id": uuid }` **or** `{ "email": string }` (exactly one), `role?` default `member` | `201 { org_id, user_id, role }` — an upsert, so re-posting changes the role |
| `DELETE` | `/v1/orgs/:id/members/:userId` | org owner or admin | — | `{ deleted: true, org_id, user_id }` |

Membership roles are `owner` · `admin` · `member`. Only the org **owner** can grant `owner` (`403` — *Only the org owner can grant the owner role.*) and the owner cannot be removed (`403` — *The org owner cannot be removed.*). Adding someone who has no codai account is `404` (*User not found.*); a non-member reading the list is `403`.

```bash
# create an org and add a teammate by email
ORG=$(curl -s -X POST https://ai.codai.ro/v1/orgs \
  -H "Authorization: Bearer $CODAI_API_KEY" -H "Content-Type: application/json" \
  -d '{ "name": "Platform team" }' | jq -r .id)

curl -X POST "https://ai.codai.ro/v1/orgs/$ORG/members" \
  -H "Authorization: Bearer $CODAI_API_KEY" -H "Content-Type: application/json" \
  -d '{ "email": "ana@example.com", "role": "admin" }'
```

Every share and org mutation writes an audit row (`share_add`, `share_remove`, `org:create`, `org:member_add`, `org:member_remove`), so the owner can always reconstruct who had access when.
