---
title: "Authentication"
description: "How API keys work, what each kind of key can reach, the device authorisation flow the CLI and desktop app use, and how keys are revoked."
canonical_url: "https://trytree.house/docs/agents/reference/authentication"
last_updated: "2026-09-29"
---

# Authentication

How API keys work, what each kind of key can reach, the device authorisation flow the CLI and desktop app use, and how keys are revoked.

## Credentials

| Credential | Sent as | Works for |
| --- | --- | --- |
| API key | `x-api-key: <key>` header | The REST API, the MCP server and the change feed |
| Browser session cookie | Cookie set by signing in | The REST API, including the key-management endpoints |

> **Use x-api-key, not Bearer**
>
> The server reads the key from the `x-api-key` header only. A key sent as `Authorization: Bearer <key>` is ignored, and the request fails with `401` as if no credential was sent. This is the most common reason a new integration can't connect.

```bash
curl https://api.trytree.house/api/workspace \
  -H "x-api-key: th_example_key"
```

## Kinds of key

| Kind | How it's made | Reaches | MCP tool set |
| --- | --- | --- | --- |
| Account-wide | Created in the web app, or `POST /api/keys` without `workspaceId` | Every workspace you're a member of, checked per request | Every tool takes `workspaceId`; adds `list_workspaces` and `create_workspace` |
| Workspace-scoped | The device flow (desktop app, CLI), or `POST /api/keys` with `workspaceId` | One workspace. Any other workspace returns `403` | No `workspaceId` parameter |

Every key also carries an **actor type**, `agent` or `human`. It is how Treehouse attributes changes: every write, move, delete and comment made with the key is recorded with that `principal`, together with the key's id and name. The activity feed shows agent changes as agent changes because the key says so, not because of which endpoint was used.

### Keys from the web app

In the web app, open the account menu and choose **API keys**. **Used by** sets the actor type:

| Used by | Actor type |
| --- | --- |
| **Agent (MCP)** | `agent` |
| **Human (CLI)** | `human` |

Keys created here are account-wide. The key is shown once, when you create it. The [API keys](https://trytree.house/docs/agents/connect/api-keys) page covers the UI in detail.

### Keys from the device flow

The desktop app and `treehouse login` get a workspace-scoped key through the [device authorisation flow](https://trytree.house/docs/agents/reference/authentication#device-authorisation-flow) below. These keys default to the `human` actor type.

## Key management endpoints

These accept a browser session cookie only. A request carrying `x-api-key` is refused with `403` and the message `Key management requires a logged-in session, not an API key`, so a leaked key can never mint or revoke other keys.

| Method | Path | Body / result |
| --- | --- | --- |
| `POST` | `/api/keys` | Body `{ name?, actorType?, workspaceId? }`. Returns `{ id, key, name, actorType }`, plus `workspaceId` when scoped. The plaintext `key` is only returned here. |
| `GET` | `/api/keys` | `{ keys: [...] }`, each with its `metadata` (`actorType`, and `workspaceId` for scoped keys). No plaintext keys. |
| `DELETE` | `/api/keys/{id}` | Revokes the key. Returns `{ ok: true }`. |

`actorType` is `agent` or `human`; anything else is treated as `human`. `name` defaults to `treehouse-key` (account-wide) or `treehouse-cli` (workspace-scoped). With `workspaceId`, you must be a member of that workspace.

## Device authorisation flow

A client with no browser session (the CLI, the desktop app, or your own tool) can get a workspace-scoped key by asking the user to approve it in the web app. It follows the shape of OAuth device authorisation (RFC 8628).

1. The client calls `POST /api/device/code` with no credentials.
2. The client shows the user `verification_uri_complete` (the web app's `/link` page, with the code filled in) and `user_code`.
3. The user signs in, picks a workspace and approves.
4. The client polls `GET /api/device/poll?device_code=<device_code>` every `interval` seconds until it gets the key.

`POST /api/device/code` returns:

```json
{
  "device_code": "…",
  "user_code": "ABCD-EFGH",
  "verification_uri": "https://app.trytree.house/link",
  "verification_uri_complete": "https://app.trytree.house/link?code=ABCD-EFGH",
  "expires_in": 600,
  "interval": 5
}
```

The code expires after 10 minutes and can be used once.

### Poll responses

| Status | Body | Meaning |
| --- | --- | --- |
| `200` | `{ "status": "pending" }` | Not approved yet. Keep polling. |
| `200` | `{ "status": "approved", "api_key", "api_key_id", "workspace_id", "api_base" }` | Approved. Store the key; this response is delivered once. |
| `400` | `device_code required` | The query parameter is missing. |
| `404` | `invalid_grant` | Unknown device code. |
| `410` | `expired_token` | The code expired. Start again. |
| `410` | `already_consumed` | The key was already delivered to an earlier poll. |
| `429` | `slow_down` | You polled sooner than `interval` after the last poll. Wait longer. |
| `403` | `access_denied` | The request was denied. |

`api_base` is the API's own public URL, so a client that started from the app URL learns where to send requests.

## Errors and limits

A missing or invalid credential returns `401`. When an API key is rejected, the error's `data` explains why:

```json
{ "error": "invalid_api_key", "reason": "INVALID_API_KEY" }
```

`reason` is the auth library's code for the specific failure (for example a key that was deleted or has expired), so a client can tell a revoked key from a transient problem.

A valid key used on a workspace it can't reach returns `403` with `Key is not scoped to this workspace` (a workspace-scoped key) or `Not a workspace member`. An unknown workspace returns `404`.

There is no per-key rate limit: the sync CLI and agents authenticate on every request. The device endpoints are limited: `POST /api/device/code` to 5 requests per 10 minutes per IP address, and approvals to 10 per 15 minutes per user. Over the limit returns `429`.

## When someone leaves a workspace

When a member is removed from a workspace, or leaves it:

- Their **workspace-scoped keys** for that workspace are deleted. A synced folder using one stops syncing and asks them to sign in again.
- Their **account-wide keys** keep working for their other workspaces, but get `403 Not a workspace member` for this one.

## Keeping keys safe

- **One key per agent or integration.** The key's name appears in the activity feed, so separate keys make it clear who did what, and you can revoke one without breaking the others.
- **Revoke keys you no longer use**, from **API keys** in the web app or with `DELETE /api/keys/{id}`.
- **Never commit a key** to a repository or paste it into a shared file. Treat it like a password: anyone holding it can read and write every workspace it reaches.
- The CLI stores its key in `<folder>/.treehouse/config.json` with file mode `0600` (readable only by you). Keep `.treehouse/` out of version control.

## Related

- [Manage API keys](https://trytree.house/docs/agents/connect/api-keys): Create a key for each agent, see which keys can reach your workspaces, and revoke the ones you no longer need.
- [MCP server](https://trytree.house/docs/agents/reference/mcp-server): Every tool the Treehouse MCP server exposes, with parameters, outputs, limits and the instructions it gives connected agents.
- [REST API](https://trytree.house/docs/agents/reference/rest-api): Endpoints for reading, writing, moving, searching, sharing and commenting on workspace files over HTTP, with headers, status codes and examples.
