# API keys and login

## Keys

Keys are managed by signed-in members in the dashboard. A key cannot create or revoke keys.

### List keys

`GET /v1/keys`

Session: member.

| Query          | Type | Meaning                     |
| -------------- | ---- | --------------------------- |
| `organization` | slug | Required. The organization. |

Response `200`:

```json
{
    "keys": [
        {
            "id": "uuid",
            "organization_id": "uuid",
            "prefix": "wb_a1b2c3d4",
            "label": "CI",
            "scopes": ["runs:read", "runs:write"],
            "created_by": "uuid",
            "expires_at": "2027-01-26T12:00:00Z",
            "last_used_at": null,
            "revoked_at": null,
            "created_at": "2026-10-28T12:00:00Z"
        }
    ]
}
```

`prefix` is the first 11 characters of the token, to identify a key. Only a hash of the token is stored.

### Create a key

`POST /v1/keys`

Session: manager.

| Field             | Type                                                                             | Meaning     |
| ----------------- | -------------------------------------------------------------------------------- | ----------- |
| `organization_id` | UUID                                                                             | Required.   |
| `label`           | string, 1 to 120 characters                                                      | Required.   |
| `scopes`          | list of `catalog:read`, `packages:write`, `runs:read`, `runs:write`, `org:admin` | Required.   |
| `expires_in_days` | integer, 1 to 365                                                                | Default 90. |

Response `201`: the key, as in the list, plus `token`, `wb_` and 64 hex characters. The token is returned once.

### Revoke a key

`DELETE /v1/keys/:id`

Session: the key's creator, or a manager of its organization. Response `200` `{ "deleted": true }`.

## Profile

`GET /v1/profile`

Any key. Sessions are refused. Returns who the key is:

```json
{
    "organization": {
        "id": "uuid",
        "slug": "acme",
        "name": "Acme",
        "description": null,
        "website": null,
        "logo_url": null,
        "accent": null,
        "verified": false,
        "personal": false,
        "created_at": "...",
        "updated_at": "..."
    },
    "user": { "id": "uuid", "email": "ada@example.com" },
    "scopes": ["catalog:read", "packages:write", "runs:read", "runs:write"],
    "key": { "id": "uuid", "label": "wb CLI", "expires_at": "..." }
}
```

`user` is `null` for a key with no owning user. `wb whoami` shows the same.

## MCP connections

Keys created by an MCP client's OAuth sign-in, for the signed-in user.

### List connections

`GET /v1/connections`

Session only.

| Query          | Type | Meaning   |
| -------------- | ---- | --------- |
| `organization` | slug | Required. |

Response `200` `{ "connections": [ { "id", "organization_id", "client_name", "connected_at", "last_used_at", "expires_at" } ] }`.

### Disconnect

`DELETE /v1/connections/:id`

Session only: the user who approved it. Revokes the key. Response `200` `{ "deleted": true }`. 404 for an ID that is not an MCP connection.

## CLI login

The device flow `wb login` uses to get a key. A login lasts 10 minutes, and the client polls at the returned `interval`.

### Start a login

`POST /v1/logins`

Open.

| Field   | Type                        | Meaning                    |
| ------- | --------------------------- | -------------------------- |
| `label` | string, 1 to 120 characters | Required. The key's label. |

Response `201`:

| Field              | Meaning                                                                |
| ------------------ | ---------------------------------------------------------------------- |
| `id`               | The login.                                                             |
| `code`             | The user code, such as `ABCD-1234`.                                    |
| `label`            | The label.                                                             |
| `verification_url` | Where the user approves.                                               |
| `expires_at`       | When the login expires.                                                |
| `interval`         | Seconds between polls, 1 to 30.                                        |
| `approved`         | `false`.                                                               |
| `secret`           | A one-time secret, 64 hex characters. Keep it; it proves the exchange. |

### Read a login

`GET /v1/logins/:id`

Open. The same fields without `secret`.

### Approve a login

`PATCH /v1/logins/:id` with `{ "organization_id": "uuid" }`

Session only. The user approves the login for one of their organizations. Response `200` `{ "approved": true }`. The dashboard's approval page does this for you.

### Exchange for a key

`POST /v1/tokens`

Open; the request proves the `secret`.

| Field      | Type              |
| ---------- | ----------------- |
| `login_id` | UUID              |
| `secret`   | 64 hex characters |

Response `200`: `{ "status": "pending" }` until approved, then:

```json
{
    "status": "complete",
    "token": "wb_...",
    "key_id": "uuid",
    "organization": {
        "id": "uuid",
        "slug": "acme",
        "name": "Acme",
        "...": "..."
    },
    "scopes": ["catalog:read", "packages:write", "runs:read", "runs:write"],
    "expires_at": "..."
}
```

The key holds every scope except `org:admin` and lasts 90 days.
