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. |
| 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.
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.
# 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
# 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:
{
"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:
{
"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.
Change feed
GET /api/workspaces/{id}/changes?sinceSeq=<n> returns every change after a cursor. See 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 | 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. |
| Workspace icon | 1 MiB |
| Search query | 200 characters |
| Change feed page | 1000 changes |
Related
- 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
The concurrency contract every Treehouse client shares - version numbers, compare-and-swap writes, conflicted copies, deletes, moves and restores.
- 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.
Last updated