---
title: "REST API"
description: "Endpoints for reading, writing, moving, searching, sharing and commenting on workspace files over HTTP, with headers, status codes and examples."
canonical_url: "https://trytree.house/docs/agents/reference/rest-api"
last_updated: "2026-09-29"
---

# REST API

Endpoints for reading, writing, moving, searching, sharing and commenting on workspace files over HTTP, with headers, status codes and examples.

## Conventions

| Convention | Detail |
| --- | --- |
| Base URL | `https://api.trytree.house` |
| Workspace routes | `/api/workspaces/{workspaceId}/…` |
| Auth | `x-api-key: <key>` or a signed-in browser session. See [authentication](https://trytree.house/docs/agents/reference/authentication). |
| Request bodies | JSON, except file writes, which send the raw bytes |
| Errors | JSON with `statusCode` and `statusMessage`. Some errors carry details in `data`; conflict and limit responses return a flat body such as `{ "error": "version_conflict", … }`. |
| Paths | Workspace-relative, `/`-separated, such as `Projects/plan.md`. Encode each segment in the URL but keep the slashes. `..`, backslashes and null bytes return `400`. |
| Private items | Another member's private file or folder behaves as if it doesn't exist (`404`). |

Find your workspace id in the web app URL (`/w/<workspaceId>/…`), with `GET /api/workspace`, or with `list_workspaces` over MCP.

## Files

| Method | Path | Purpose |
| --- | --- | --- |
| `GET` | `files/{path}` | Read a file's bytes |
| `PUT` | `files/{path}` | Create or update a file |
| `DELETE` | `files/{path}` | Delete a file |
| `GET` | `list?prefix=` | List files |
| `POST` | `move` | Move or rename one file |
| `POST` | `move-folder` | Carry a folder's own privacy, icon and order to a new path |
| `GET` | `version?path=&version=` | Read an earlier version |
| `POST` | `restore` | Restore an earlier version as a new version |
| `GET` | `search?q=&limit=` | Fuzzy search over names and paths |

All paths in this section are relative to `/api/workspaces/{workspaceId}/`.

### Read a file

`GET files/{path}` returns the raw bytes with these headers:

| Header | Value |
| --- | --- |
| `X-Treehouse-Version` | The file's current version. Prefer this header. |
| `ETag` | The same version, quoted (`"3"`). A CDN may weaken or strip it, so don't rely on it. |
| `X-Treehouse-Content-Hash` | SHA-256 of these exact bytes. Unlike a version number, it is never reused. |
| `Content-Type` | The stored content type. |

Send `If-None-Match: "<version>"` to get `304 Not Modified` when you already have that version. Add `?download=1` to get a `Content-Disposition: attachment` response. Errors: `404` not found, `502` if the stored bytes are missing.

```bash
curl -i https://api.trytree.house/api/workspaces/$WS/files/Projects/plan.md \
  -H "x-api-key: $TREEHOUSE_API_KEY"
```

### Write a file

`PUT files/{path}` with the complete file as the request body. Choose one precondition:

| Header | Behaviour |
| --- | --- |
| `If-Match: "<n>"` | Update, only if the current version is `n`. Quotes are optional. |
| `If-None-Match: *` | Create only. If the path exists with different content, your bytes become a conflicted copy (`412`). |
| Neither | Create if the path doesn't exist; `428` if it does. |

The `Content-Type` you send is stored with the file. If you send none, it is guessed from the extension. Note that `curl --data-binary` sends `application/x-www-form-urlencoded` unless you set the header yourself.

| Status | Body | Meaning |
| --- | --- | --- |
| `201` | `{ "path", "version" }` | Created |
| `200` | `{ "path", "version" }` | Updated. The new version is also in `ETag`. |
| `400` | `Empty request body`, `Invalid If-Match`, or a path error | Nothing written |
| `402` | `{ "error": "workspace_read_only" }` | The subscription has lapsed. Reads, moves and deletes still work. |
| `404` |  | You can't write to that path (another member's private item) |
| `412` | `{ "error": "version_conflict", "current_version", "conflict_copy_path" }` | Stale `If-Match`. Your bytes were saved at `conflict_copy_path`. |
| `413` | `data: { "error": "file_too_large", "limit_bytes" }` | Larger than the per-file limit |
| `428` | `If-Match required for existing file` | The path exists and you sent no precondition |
| `507` | `{ "error": "quota_exceeded", "used_bytes", "limit_bytes" }` | Storage limit reached |
| `507` | `{ "error": "file_count_exceeded", "used_files", "limit_files" }` | File limit reached |

A stale write with content identical to the current file succeeds without creating a copy. See [versions and conflicts](https://trytree.house/docs/agents/handbook/versions-and-conflicts).

```bash
# Update version 3 of a file
curl -X PUT https://api.trytree.house/api/workspaces/$WS/files/Projects/plan.md \
  -H "x-api-key: $TREEHOUSE_API_KEY" \
  -H 'If-Match: "3"' \
  -H "Content-Type: text/markdown" \
  --data-binary @plan.md
```

```bash
# Create a new file, failing safely if it already exists
curl -X PUT https://api.trytree.house/api/workspaces/$WS/files/Inbox/idea.md \
  -H "x-api-key: $TREEHOUSE_API_KEY" \
  -H "If-None-Match: *" \
  -H "Content-Type: text/markdown" \
  --data-binary @idea.md
```

### Delete a file

`DELETE files/{path}` with `If-Match: "<n>"` (required, `428` without it). Returns `204`. If the file has changed since version `n`, nothing is deleted and you get `412` with `{ "error": "version_conflict", "current_version" }`.

### List files

`GET list?prefix=Projects` returns every entry under the prefix in one response:

```json
{
  "entries": [
    { "path": "Projects/plan.md", "size": 1204, "contentType": "text/markdown", "version": 3 }
  ],
  "icons": { "Projects": "📁" },
  "orders": { "Projects/plan.md": 0 },
  "privatePaths": [],
  "nextCursor": null
}
```

Omit `prefix` for the whole workspace. Entries include `.keep` files, the hidden placeholders that keep empty folders in place. `privatePaths` lists items that are private to you. `limit` and `cursor` are accepted for forward compatibility but currently have no effect, and `nextCursor` is always `null`. If you page, keep requesting until `nextCursor` is `null`.

### Move a file

`POST move` with `{ "from", "to", "baseVersion"? }`. Returns `{ "path", "version": 1 }`: a moved file restarts at version 1. With `baseVersion`, the move only happens if the source is still at that version (`412` otherwise). `404` if the source is missing, `409` if the destination exists.

To move a folder, move each file, then call `POST move-folder` with `{ "from", "to" }` so the folder's own privacy setting, icon and ordering follow it. `move-folder` does not move any files.

### Versions and restore

`GET version?path=Projects/plan.md&version=2` returns that version's bytes, with `ETag` set to the version. `404` if the path or version isn't available.

`POST restore` with `{ "path", "targetVersion" }` writes that version's bytes as a new version and returns `{ "path", "version", "restoredFrom" }`. If concurrent writes keep winning, it returns `409` with `{ "error": "restore_conflicted", "current_version", "conflict_copy_path" }` and the restored bytes are in the conflicted copy.

### Search

`GET search?q=clinot&limit=20` fuzzy-matches file and folder paths (not contents), ranked best first:

```json
{
  "results": [
    { "path": "Clients/acme/notes.md", "name": "notes.md", "isDir": false, "score": 41.2 }
  ],
  "totalMatches": 1,
  "truncated": false
}
```

`q` is required and at most 200 characters. `limit` defaults to 50 and is capped at 200. There is no offset: narrow the query instead.

## Workspace

| Method | Path | Purpose |
| --- | --- | --- |
| `GET` | `/api/workspaces/{id}` | `{ id, name, icon, color, theme, themeCss }` |
| `PATCH` | `/api/workspaces/{id}` | Update `name` (1-100 characters), `icon`, `color`, `theme`, `themeCss`. An empty string or `null` clears a field. |
| `GET` | `/api/workspaces/{id}/usage` | `usedBytes`, `limitBytes`, `remainingBytes`, `usedFiles`, `limitFiles`, `remainingFiles`, `warning`, `maxFileBytes`, share-view counts and `billing`. Limits are `null` when unlimited. |
| `GET` | `/api/workspaces/{id}/members` | `{ members: [{ id, name, image, role }] }`. No email addresses. |
| `DELETE` | `/api/workspaces/{id}/members/{userId}` | Remove a member. Browser session only; owners and admins only. The last owner can't be removed (`409`). |
| `PUT` | `/api/workspaces/{id}/node-icon` | `{ path, icon }` sets a file or folder icon; empty `icon` clears it. |
| `PUT` | `/api/workspaces/{id}/node-private` | `{ path, kind, private }` makes an item private to you or shared again. |
| `PUT` | `/api/workspaces/{id}/node-order` | `{ paths: [...] }` sets a custom display order (up to 1000 paths). |
| `POST` | `/api/workspaces/{id}/icon` | Raw image bytes (up to 1 MiB) as the workspace icon. |

## Share links

| Method | Path | Purpose |
| --- | --- | --- |
| `GET` | `/api/workspaces/{id}/shares?path=` | `{ shares, inherited }`: shares on this path, and folder shares above it that also cover it |
| `POST` | `/api/workspaces/{id}/shares` | Create a link. Body `{ path, kind, access, password?, pinnedVersion?, pinnedContentHash?, allowScripts? }`. Returns `201 { share }`. |
| `PATCH` | `/api/workspaces/{id}/shares/{shareId}` | `{ allowScripts }` turns script execution on or off for an HTML link |
| `DELETE` | `/api/workspaces/{id}/shares/{shareId}` | Revoke a link |

`kind` is `file` or `folder`; `access` is `member` or `public`. **API keys can only create member links.** Public links, password-protected links, and turning scripts on require a signed-in browser session; with a key they return `403`. A key can turn scripts off. Private items can't be shared (`409`), and folder links can never run scripts (`400`).

## Comments

| Method | Path | Purpose |
| --- | --- | --- |
| `GET` | `/api/workspaces/{id}/comments` | List threads |
| `POST` | `/api/workspaces/{id}/comments` | Start a thread |
| `POST` | `/api/workspaces/{id}/comments/{threadId}/replies` | Reply. Body `{ body, dedupeKey? }`. `201 { entry, deduped }`. |
| `PUT` | `/api/workspaces/{id}/comments/{threadId}/resolved` | Body `{ "resolved": true }` to resolve, `false` to reopen. Idempotent. |
| `GET` | `/api/workspaces/{id}/comments/{threadId}/pinned-content` | The exact markdown the thread was written against, as a download. `410` if it's gone. |

The list accepts the same filters as the MCP `list_comments` tool, as query parameters: `path`, `threadId`, `status`, `anchorStatus`, `author`, `limit`, `offset`, `entriesLimit`, `entriesOffset`. It returns `{ threads, total, nextOffset, truncated }`.

Creating a thread takes `{ path, body, kind, quote?, contextBefore?, contextAfter?, occurrence?, baseVersion? }`. Unlike MCP, `kind` (`range` or `point`) is required here. Success is `201 { thread }`. A quote that can't be placed returns `409` with `anchor_ambiguous` (plus `occurrences`), `anchor_not_found` or `base_version_conflict`. A non-markdown file returns `400 { "error": "not_markdown" }`, and a read-only workspace `402`. Size limits match the [MCP server](https://trytree.house/docs/agents/reference/mcp-server#limits).

## Change feed

`GET /api/workspaces/{id}/changes?sinceSeq=<n>` returns every change after a cursor. See [change feed](https://trytree.house/docs/agents/reference/change-feed).

## Account and top-level endpoints

| Method | Path | Auth | Purpose |
| --- | --- | --- | --- |
| `GET` | `/api/health` | None | `{ status: "ok", timestamp }` |
| `GET` | `/api/plans` | None | `{ available, plans }`, the plans on offer. `available: false` on servers without billing. |
| `GET` | `/api/me` | Key or session | `{ user }`, the account behind the credential |
| `GET` | `/api/workspace` | Key or session | `{ id, name }`: a workspace-scoped key's workspace, otherwise your oldest workspace |
| `GET` | `/api/workspaces` | Session only | `{ workspaces: [...] }`, every workspace you're a member of |
| `POST` | `/api/workspaces` | Session only | Create a workspace. Body `{ name, icon?, color? }`. |
| `POST` | `/api/invites` | Session only | `{ workspaceId, expiresInDays?, maxUses? }` returns an invite `url` (expiry 7 days by default, 90 at most) |
| `GET` | `/api/invites/{token}` | None | Preview an invite |
| `POST` | `/api/invites/{token}/accept` | Session only | Join the workspace |
| `*` | `/api/keys`, `/api/device/*` | See [authentication](https://trytree.house/docs/agents/reference/authentication) | Keys and the device flow |

To list workspaces with an API key, use the MCP `list_workspaces` tool with an account-wide key.

## Content types

When a write doesn't send a `Content-Type`, the file's type comes from its extension. Anything else is stored as `application/octet-stream`.

| Extension | Content type |
| --- | --- |
| `.md`, `.markdown` | `text/markdown` |
| `.txt` | `text/plain` |
| `.json` | `application/json` |
| `.html`, `.htm` | `text/html` |
| `.css` | `text/css` |
| `.js` | `text/javascript` |
| `.ts` | `text/typescript` |
| `.csv` | `text/csv` |
| `.svg` | `image/svg+xml` |
| `.png` | `image/png` |
| `.jpg`, `.jpeg` | `image/jpeg` |
| `.gif` | `image/gif` |
| `.webp` | `image/webp` |
| `.pdf` | `application/pdf` |
| `.zip` | `application/zip` |

## Size limits

| Limit | Value |
| --- | --- |
| Single file | Your plan's per-file limit, and never more than the server's ceiling (100 MB unless the operator changes it). Oversized uploads are rejected before they are stored. |
| Storage and file count | Per workspace, by plan. See [plans and limits](https://trytree.house/docs/account/plans-and-limits). |
| Workspace icon | 1 MiB |
| Search query | 200 characters |
| Change feed page | 1000 changes |

## Related

- [Authentication](https://trytree.house/docs/agents/reference/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.
- [Versions and conflicts](https://trytree.house/docs/agents/handbook/versions-and-conflicts): The concurrency contract every Treehouse client shares - version numbers, compare-and-swap writes, conflicted copies, deletes, moves and restores.
- [Change feed](https://trytree.house/docs/agents/reference/change-feed): Read every change to a workspace in order with a cursor, so a script or agent can react to new and edited files.
