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

# Organizations and members

> The tenancy boundary, the code that identifies it, and who gets to be inside.

An **organization** is the tenancy boundary. Every row that belongs to a customer
hangs off `organization_id`, and every query filters on it.

A user reaches an organization through a **membership**, which carries exactly one
role. One user can hold memberships in many organizations; the panel's switcher is
that list.

## The public code

Every organization has a `public_code` shaped `org_<32 hex>`. It is what appears
in the panel URL and what travels in the `X-Organization-Id` header.

```
/org_4b1e9c…/settings/members
```

It is opaque and immutable on purpose: renaming an organization must never break a
link. And it **identifies** without authorizing — pasting someone else's code into
the URL resolves no session, because the membership JOIN matches nothing.

<Note>
  The internal UUID never leaves the API on the surfaces the panel uses. Use
  `public_code` in URLs and headers.
</Note>

## Creating one

Sign-up creates the first organization. A signed-in user can open more, becoming
owner of each:

<CodeGroup>
  ```bash cURL theme={null}
  curl -s $API/v1/organizations \
    -H "Authorization: Bearer $UK_SESSION" \
    -H 'Content-Type: application/json' \
    -d '{ "name": "Analytical Engine" }'
  ```

  ```json Response — 201 theme={null}
  {
    "organization": {
      "id": "…",
      "name": "Analytical Engine",
      "public_code": "org_4b1e…",
      "onboarded": false
    },
    "active_organization_id": "…",
    "active_organization_code": "org_4b1e…"
  }
  ```
</CodeGroup>

The session switches to the new organization immediately — nobody creates a
workspace and then goes looking for it.

Three invariants are established in that same transaction, and they hold for the
organization's whole life:

* it always has the three system roles (`owner`, `admin`, `member`);
* it always has an owner;
* it always has a live and a test environment, each with a publishable key.

## Switching

Switching is navigation, not state you have to manage. Membership is verified
every time, and switching into an organization you do not belong to answers `404`
— never a leak that it exists.

```bash theme={null}
curl -s -X PUT $API/v1/session/organization \
  -H "Authorization: Bearer $UK_SESSION" \
  -H 'Content-Type: application/json' \
  -d '{ "organization_id": "…" }'
```

## Inviting people

An invitation is created against a role and emailed as a one-time link, valid for
7 days. One pending invitation per address, enforced by a partial unique index —
so a double click cannot produce two.

<CodeGroup>
  ```bash Invite theme={null}
  curl -s $API/v1/organization/members/invitations \
    -H "Authorization: Bearer $UK_SESSION" \
    -H "X-Organization-Id: org_4b1e…" \
    -H 'Content-Type: application/json' \
    -d '{ "email": "grace@example.com", "role_id": "…" }'
  ```

  ```bash Read (public) theme={null}
  curl -s "$API/v1/invitations?token=uk_inv_…"
  ```

  ```bash Accept (public) theme={null}
  curl -s $API/v1/invitations/accept \
    -H 'Content-Type: application/json' \
    -d '{ "token": "uk_inv_…", "name": "Grace Hopper", "password": "Compiler1" }'
  ```
</CodeGroup>

Reading the invitation tells the accept screen whether the address already has an
account (`existing_account`), so it knows whether to ask for a name and password.
An existing account joins directly — the invitation went to that address, and
possession of the link proves nothing more is needed.

The invitation exists even when the email is lost. Revoke and re-invite.

## Leaving and removing

Two different actions, deliberately:

| Action              | Endpoint                                   | Permission              |
| ------------------- | ------------------------------------------ | ----------------------- |
| Remove someone else | `DELETE /v1/organization/members/{userId}` | `members:write`         |
| Leave yourself      | `DELETE /v1/organization/members/me`       | none — every member may |

Leaving is a user administering their own access, so it is not gated on
`members:write`. Removing yourself through the first endpoint answers `403` with a
message pointing at the second.

Removal takes effect on that person's very next request: the session's membership
JOIN stops matching.

### The last-owner guard

An organization must keep at least one owner. Demoting, removing or leaving as the
last owner answers:

```json 409 theme={null}
{ "error": { "code": "last_owner", "message": "the organization must keep at least one owner" } }
```

The guard locks the owner rows before counting, so two concurrent demotions of
different owners serialize instead of both reading "2 owners" and leaving the
organization ownerless.

## Deleting

Owner only (`organization:delete`), and the exact name must be typed back:

```bash theme={null}
curl -s -X DELETE $API/v1/organization \
  -H "Authorization: Bearer $UK_SESSION" \
  -H "X-Organization-Id: org_4b1e…" \
  -d '{ "name": "Analytical Engine" }'
```

Everything cascades: memberships, roles, keys, invitations, environments, contacts,
and the sessions pinned to it. A mismatched name answers `400 confirmation_mismatch`.
