> ## 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.

# Authentication

> Authenticate to the Bonya API with a personal API key or a session JWT

## Base URL

```
https://api.bonya.ai
```

## Two ways to authenticate

The API accepts two bearer-token schemes, and most operations accept either one:

> [card] Personal API key
>
> A `byk_`-prefixed personal API key. A user-principal credential, resolved to a tenant (organization) by org context. Used by the CLI, SDKs, and most direct API calls.

> [card] Supabase session JWT
>
> A Supabase-issued session JWT. Required for organization administration, invites, and API-key management — these reject personal API keys outright.

Either way, the token goes in the standard header:

```
Authorization: Bearer <token>
```

### What needs which

Sandbox, session, exec, file, and preview operations accept a `personalApiKey`. Organization administration (listing/creating/renaming organizations, managing members), invitations, and API-key management are **JWT-only** — they explicitly refuse personal API keys, because they're dashboard-facing operations where a signed-in browser session is doing the asking, not an automated client.

Device-login's mint and poll steps (below) are unauthenticated entirely — the token they hand you *is* the credential at that point, there's nothing to bear a bearer token for yet.

## Organization context

```
X-Bonya-Organization-ID: <organization_id>
```

Most sandbox-scoped operations accept this optional header to select which organization (tenant) the call operates against:

* **Omitted** — resolves to the caller's personal organization.
* **Malformed** — `400`.
* **Well-formed but the caller isn't a member** — `404`, not `403`. The API doesn't confirm an organization exists to a non-member.

## Environment variables

The CLI and SDKs read these environment variables:

| Variable | Purpose |
| - | - |
| `BONYA_API_KEY` | The personal API key to authenticate with. |
| `BONYA_CA_BUNDLE` | Path to a CA bundle, for custom TLS trust roots. |
| `BONYA_ORGANIZATION_ID` | Default value sent as `X-Bonya-Organization-ID` on every request. |

```bash theme={null}
export BONYA_API_KEY=byk_...
export BONYA_ORGANIZATION_ID=org-...
```

> [!NOTE]
>
> The CLI also accepts `--endpoint`/`BONYA_ENDPOINT` to point at a non-default deployment — see [CLI configuration](https://docs.bonya.ai/cli-reference/configuration.md). The SDKs always talk to `https://api.bonya.ai` and don't document an endpoint override.

## Getting a key: device login

`bonya login` uses a three-leg device-login flow: mint a token, approve it in a browser against your already-signed-in dashboard session, then poll until it's approved. No password ever passes through the CLI.

**1. Mint a device-login token**

`POST /v1/device-login` — unauthenticated, no request body.

Returns a `DeviceLoginMintResponse`:

**token** `string` *required*

The `byd_`-prefixed device-login token. It's also embedded in `verification_url`.

**verification\_url** `string` *required*

The dashboard URL the CLI opens a browser to.

**expires\_in** `integer` *required*

Seconds until the token expires unapproved.

**interval** `integer` *required*

Recommended seconds to wait between poll calls.

```shell theme={null}
curl -s -X POST https://api.bonya.ai/v1/device-login
```

```json theme={null}
{
  "token": "byd_...",
  "verification_url": "https://bonya.ai/device?token=byd_...",
  "expires_in": 600,
  "interval": 5
}
```

**2. Approve in the browser**

The CLI opens `verification_url`. The signed-in dashboard session calls `POST /v1/device-login/approve` with the token, authenticated with the browser's own **Supabase JWT** — this is a JWT-only, JWT-provisioning operation, since it's the already-signed-in session vouching for a login it didn't itself start.

Request body (`DeviceLoginApproveRequest`):

**token** `string` *required*

The `byd_` device-login token from the verification URL.

A successful approval returns `204` with no body.

**3. Poll for the API key**

`POST /v1/device-login/poll` — unauthenticated; the token itself is the credential. Same request body shape as approve (`{ "token": "byd_..." }`).

Always returns `200` with a `DeviceLoginPollResponse`:

**status** `string` *required*

One of `pending`, `approved`, or `expired`.

**api\_key** `string`

Present **only once** — on the single poll response that first observes `approved`.

**user\_email** `string`

The approving user's email. Present alongside `api_key`.

An unknown token is `404`. Poll every `interval` seconds (from the mint response) until `status` is `approved` or `expired`.

```shell theme={null}
curl -s -X POST https://api.bonya.ai/v1/device-login/poll \
  -H "Content-Type: application/json" \
  -d '{"token": "byd_..."}'
```

> [!WARNING]
>
> `api_key` is returned exactly once, on the poll call that first sees `approved`. Store it immediately (the CLI writes it to its config) — polling again afterward returns `approved` without the key.

## Using the key

```bash theme={null}
curl -s https://api.bonya.ai/v1/sandbox \
  -H "Authorization: Bearer $BONYA_API_KEY"
```

Every SDK reads `BONYA_API_KEY` from the environment by default, or accepts an explicit override in code.

## Managing API keys

Personal API keys themselves are managed over the JWT-only surface — a key can never be used to manage keys:

* `GET /v1/api-keys` — list your keys (prefix and last 4 characters only; JWT required).
* `POST /v1/api-keys` — create a new key. The `secret` is shown exactly once in the response.
* `DELETE /v1/api-keys/{key_id}` — revoke a key.

This is the dashboard/JWT path for issuing a key directly, as an alternative to the device-login flow above.


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