REST API

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

Conventions

ConventionDetail
Base URLhttps://api.trytree.house
Workspace routes/api/workspaces/{workspaceId}/…
Authx-api-key: <key> or a signed-in browser session. See authentication.
Request bodiesJSON, except file writes, which send the raw bytes
ErrorsJSON with statusCode and statusMessage. Some errors carry details in data; conflict and limit responses return a flat body such as { "error": "version_conflict", … }.
PathsWorkspace-relative, /-separated, such as Projects/plan.md. Encode each segment in the URL but keep the slashes. .., backslashes and null bytes return 400.
Private itemsAnother 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

MethodPathPurpose
GETfiles/{path}Read a file's bytes
PUTfiles/{path}Create or update a file
DELETEfiles/{path}Delete a file
GETlist?prefix=List files
POSTmoveMove or rename one file
POSTmove-folderCarry a folder's own privacy, icon and order to a new path
GETversion?path=&version=Read an earlier version
POSTrestoreRestore an earlier version as a new version
GETsearch?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:

HeaderValue
X-Treehouse-VersionThe file's current version. Prefer this header.
ETagThe same version, quoted ("3"). A CDN may weaken or strip it, so don't rely on it.
X-Treehouse-Content-HashSHA-256 of these exact bytes. Unlike a version number, it is never reused.
Content-TypeThe 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.

terminal
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:

HeaderBehaviour
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).
NeitherCreate 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.

StatusBodyMeaning
201{ "path", "version" }Created
200{ "path", "version" }Updated. The new version is also in ETag.
400Empty request body, Invalid If-Match, or a path errorNothing written
402{ "error": "workspace_read_only" }The subscription has lapsed. Reads, moves and deletes still work.
404You 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.
413data: { "error": "file_too_large", "limit_bytes" }Larger than the per-file limit
428If-Match required for existing fileThe 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.

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

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

MethodPathPurpose
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}/usageusedBytes, 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}/iconRaw image bytes (up to 1 MiB) as the workspace icon.
MethodPathPurpose
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}/sharesCreate 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

MethodPathPurpose
GET/api/workspaces/{id}/commentsList threads
POST/api/workspaces/{id}/commentsStart a thread
POST/api/workspaces/{id}/comments/{threadId}/repliesReply. Body { body, dedupeKey? }. 201 { entry, deduped }.
PUT/api/workspaces/{id}/comments/{threadId}/resolvedBody { "resolved": true } to resolve, false to reopen. Idempotent.
GET/api/workspaces/{id}/comments/{threadId}/pinned-contentThe 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

MethodPathAuthPurpose
GET/api/healthNone{ status: "ok", timestamp }
GET/api/plansNone{ available, plans }, the plans on offer. available: false on servers without billing.
GET/api/meKey or session{ user }, the account behind the credential
GET/api/workspaceKey or session{ id, name }: a workspace-scoped key's workspace, otherwise your oldest workspace
GET/api/workspacesSession only{ workspaces: [...] }, every workspace you're a member of
POST/api/workspacesSession onlyCreate a workspace. Body { name, icon?, color? }.
POST/api/invitesSession only{ workspaceId, expiresInDays?, maxUses? } returns an invite url (expiry 7 days by default, 90 at most)
GET/api/invites/{token}NonePreview an invite
POST/api/invites/{token}/acceptSession onlyJoin the workspace
*/api/keys, /api/device/*See authenticationKeys 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.

ExtensionContent type
.md, .markdowntext/markdown
.txttext/plain
.jsonapplication/json
.html, .htmtext/html
.csstext/css
.jstext/javascript
.tstext/typescript
.csvtext/csv
.svgimage/svg+xml
.pngimage/png
.jpg, .jpegimage/jpeg
.gifimage/gif
.webpimage/webp
.pdfapplication/pdf
.zipapplication/zip

Size limits

LimitValue
Single fileYour 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 countPer workspace, by plan. See plans and limits.
Workspace icon1 MiB
Search query200 characters
Change feed page1000 changes
  • 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