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

# Organizations

> Multi-tenant context and administration

An **organization** is the tenant a sandbox, preview, or other resource belongs to. Every caller has an implicit **personal organization**; callers can also belong to named organizations shared with others.

## Personal vs. named organizations

`personal: true` marks the deterministic tenant an omitted `X-Bonya-Organization-ID` header resolves to — see [Authentication](https://docs.bonya.ai/authentication.md#organization-context) for how that header selects org context on sandbox-scoped calls. Personal organizations can't be invited into; membership is just the one owner.

Named organizations are created explicitly (`POST /v1/organizations`) and can have multiple members with different roles.

### Organization fields

**organization\_id** `string` *required*

Matches `^org-[0-9a-f]{32}$`.

**name** `string` *required*

Display name, up to 128 characters.

**personal** `boolean` *required*

Whether this is the caller's personal organization.

**role** `string`

The requesting caller's role, when known.

**created\_at** `string` *required*

Creation timestamp.

## Roles

Roles are a closed set — `owner` or `member`, defaulting to `member`. There are no custom roles.

> [!WARNING]
>
> An organization's **final owner** can't be demoted or removed — both return `409 final_owner`. This is also why the sole owner of a personal organization can't leave it: a personal organization always has exactly one member, who is always its owner.

## Organization context in the SDKs

Every SDK reads an organization id from the environment (`BONYA_ORGANIZATION_ID`) or accepts an explicit override at client construction, and resends it as `X-Bonya-Organization-ID` on every control-plane call. All three deliberately treat an **explicitly empty** value as an error rather than a silent fallback to your personal organization — `BONYA_ORGANIZATION_ID=""`, or the equivalent constructor argument set to `""`, raises immediately instead of quietly resolving to your personal tenant.

This matters most in CI: an unset variable expanded into another one often evaluates to an empty string, and running a shared pipeline's jobs against whichever engineer's personal organization happens to own the credential is a worse failure mode than an immediate, loud error. Set `BONYA_ORGANIZATION_ID` explicitly in CI rather than relying on the personal-organization default.

## Administration is JWT-only

> [!NOTE]
>
> Organization administration — creating, renaming, and reading organizations; listing, changing the role of, or removing members — requires a `supabaseJwt`. Personal API keys (`byk_`) are rejected outright on these operations. A `byk_` key can still *set org context* via `X-Bonya-Organization-ID` on sandbox-scoped calls (see [Authentication](https://docs.bonya.ai/authentication.md)); it just can't administer organizations itself. These are dashboard-facing operations, not something an automated client using an API key is expected to call directly.

The core administrative operations:

| Operation | Notes |
| - | - |
| `GET /v1/organizations` | Organizations the caller belongs to, with their role in each. |
| `POST /v1/organizations` | Create an organization owned by the caller. |
| `GET /v1/organizations/{organization_id}` | Any member may read. A non-member gets `404`, never `403`. |
| `PATCH /v1/organizations/{organization_id}` | Rename. Owner only. There is no organization delete operation. |
| `GET /v1/organizations/{organization_id}/members` | Any member may list. |
| `PATCH /v1/organizations/{organization_id}/members/{user_id}` | Change a member's role. Owner only. |
| `DELETE /v1/organizations/{organization_id}/members/{user_id}` | Administrative removal (owner only) and a member's own self-leave share this same call. |

## Invitations

Adding someone to a named organization goes through an invitation, not a direct membership write. This surface is dashboard-facing: four of its operations are marked internal in the API contract (`x-internal: true`), so treat invitations as a concept worth understanding rather than a REST flow to build automation against.

Conceptually:

* An owner creates an invitation addressed to an email with a role. The raw token is returned exactly once, and the dashboard — not the API — builds the acceptance link, since the server doesn't know the dashboard's origin.
* A live invitation already addressed to the same email is a conflict; a personal organization can't be invited into at all.
* Acceptance lives at a **separate, top-level path**, not nested under the organization — because the token itself is the credential, the same pattern used elsewhere for one-time bearer tokens. The caller must be signed in as the invited address; the check is case-insensitive.
* Accepting, once, atomically provisions membership and consumes the token. Unknown, wrong-recipient, already-consumed, revoked, and expired tokens are all a single indistinguishable `404` — a mismatched attempt doesn't invalidate the invitation for its real recipient.

All of this is JWT-only, same as the rest of organization administration.

## Related

> [card] [Authentication](https://docs.bonya.ai/authentication.md)

> [card] [API keys](https://docs.bonya.ai/concepts/api-keys.md)

> [card] [Errors](https://docs.bonya.ai/concepts/errors.md)


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.