Build
Files and versions
Spaces, paths and nodes; uploading and downloading through the block gateway; version history, names and comparisons.
Spaces and paths
curl -H "Authorization: Bearer $SIDESPACE_TOKEN" https://side.example.com/v1/spacesEach space has a root_node_id. Below it, every file and folder is a node with an id (nod_…). You can work by id or by path:
| To | Call |
|---|---|
| Find the node at a path | GET /v1/spaces/{sid}/paths/Ep01/Shots/A001_C003.mov |
| List a folder (by name, paged) | GET /v1/spaces/{sid}/nodes/{nid}/children?limit=200&cursor=… |
| Create a folder or an empty file | POST /v1/spaces/{sid}/nodes with parent_id, name, kind (dir or file) |
| Rename or move | PATCH /v1/spaces/{sid}/nodes/{nid} with name and/or parent_id |
| Delete (to the trash, 30 days) | DELETE /v1/spaces/{sid}/nodes/{nid} |
| Restore from the trash | POST /v1/spaces/{sid}/nodes/{nid}/restore |
Names are unique in a folder, ignoring case. When you create a node whose name is taken, the default if_exists: "rename" makes A001_C003 (2).mov; if_exists: "reuse" returns the existing node instead, so two programs saving the same path end up with one file and two versions.
PATCH takes an optional If-Match: <meta_rev> header. If someone renamed or moved the node since you read it, you get 412 instead of overwriting their change.
The SDKs and the CLI take paths everywhere and resolve them for you:
sidespace ls "Film A" Ep01/Shots
sidespace mv "Film A" Ep01/old.mov Ep01/Archive/old.mov
sidespace rm "Film A" Ep01/tmpHow bytes move
The API server never carries file content. It hands out short tickets for the block gateway, which stores content as encrypted, deduplicated chunks (content-defined, BLAKE3-hashed) and streams it.
- Uploading the same footage twice, or a re-export that changed only the metadata, sends only the chunks the gateway doesn't have.
- Reads support HTTP
Range, and the gateway reads only the chunks a range touches. A player can start in the middle of a 200 GB file at once. - Tickets last 15 minutes, or 6 hours with
"stream": truefor a player.
Upload
The SDKs and the CLI do all of this in one call (side.upload(space, path, data)). Over plain HTTP, an upload is three steps.
1. Get the node (create it, or reuse the file already at that name):
curl -X POST https://side.example.com/v1/spaces/$SPACE/nodes \
-H "Authorization: Bearer $SIDESPACE_TOKEN" -H "Content-Type: application/json" \
-d '{"parent_id": "'$FOLDER'", "name": "A001_C003.mov", "kind": "file", "if_exists": "reuse"}'2. Send the bytes. Open an upload session to get a put ticket, then stream the whole file to the gateway. It chunks and stores it and answers with the manifest.
curl -X POST https://side.example.com/v1/spaces/$SPACE/upload-sessions \
-H "Authorization: Bearer $SIDESPACE_TOKEN" -H "Content-Type: application/json" -d '{"hashes": []}'
# {"missing": [], "ticket": "…", "blockgw_url": "https://blocks.side.example.com", "expires_at": "…"}
curl -X POST "https://blocks.side.example.com/stream-upload?t=$TICKET" --data-binary @A001_C003.mov
# {"algo": "fastcdc-v1", "size": 734003200, "chunks": [{"hash": "9f2c…", "offset": 0, "len": 1048576}, …]}Clients that chunk locally (the Mac app and the CLI do) send their chunk hashes in hashes, upload only the missing ones with PUT /chunks/{hash}?t=…, and build the manifest themselves.
3. Commit the version:
curl -X POST https://side.example.com/v1/spaces/$SPACE/nodes/$NODE/versions \
-H "Authorization: Bearer $SIDESPACE_TOKEN" -H "Content-Type: application/json" \
-d '{"base_version_id": "ver_…", "manifest": {…}, "mime": "video/quicktime", "client_mtime": "2026-11-14T09:30:00Z"}'base_version_id is the version you started from (null for a new file). If someone else saved in between, your version still becomes current (last save wins), the answer's superseded names the version you replaced, and its author gets a notification. Nothing is lost: both are in the history. A missing chunk answers 409 missing_chunks.
Download
Ask for a ticket, then read from the gateway:
curl -X POST https://side.example.com/v1/spaces/$SPACE/download-tickets \
-H "Authorization: Bearer $SIDESPACE_TOKEN" -H "Content-Type: application/json" \
-d '{"version_id": "ver_…"}'
# {"ticket": "…", "manifest": {…}, "blockgw_url": "https://blocks.side.example.com", "expires_at": "…"}
curl -H "Range: bytes=0-1048575" "https://blocks.side.example.com/files/ver_…?t=$TICKET" -o head.binAdd download=<file name> to the URL to get a Content-Disposition: attachment header, which is handy for links in a browser. The gateway answers CORS for /files/*, so a web page can play or fetch a file directly.
const bytes = await side.download("Film A", "/Ep01/notes.md");
const res = await side.open("Film A", "/Ep01/Shots/A001_C003.mov", { range: [0, 1_048_575] }); // a fetch Responsedata = side.download("Film A", "/Ep01/notes.md")
with side.open("Film A", "/Ep01/Shots/A001_C003.mov", range=(0, 1048575)) as r:
head = r.read()Renditions
The server makes a thumb (poster image) for images and videos, and a proxy for videos: a 720p H.264 stream that plays at once in any browser, whatever the original's codec or size. Ask for one with "rendition": "thumb" or "proxy" in the ticket request. Your own tools can attach renditions with PUT /v1/spaces/{sid}/versions/{vid}/renditions/{kind} (scope renditions:write).
Versions
sidespace versions "Film A" Ep01/cut.mp4
sidespace get "Film A" Ep01/cut.mp4 cut_v3.mp4 --version 3
sidespace label "Film A" Ep01/cut.mp4 3 "Client cut"
sidespace compare "Film A" Ep01/cut.mp4 "Client cut" # against the current version
sidespace restore "Film A" ver_01JD…| To | Call |
|---|---|
| History, newest first | GET /v1/spaces/{sid}/nodes/{nid}/versions |
| Make an old version current again (a copy, history kept) | POST /v1/spaces/{sid}/versions/{vid}/restore |
Name a version ("Client cut"), or clear it with "" | PUT /v1/spaces/{sid}/versions/{vid}/label |
| What changed between two versions | GET /v1/spaces/{sid}/versions/{a}/compare/{b} |
A name belongs to one version of a file at a time, ignoring case: naming another version "Client cut" moves the name. Anywhere the CLI and MCP take a version you can give an id, a number (3 or v3) or a name.
Compare answers without downloading anything, from the manifests and sidecars:
{
"same_content": false,
"size_delta": -1048576,
"bytes": { "shared": 690000000, "only_a": 44003200, "only_b": 42954624, "shared_ratio": 0.94 },
"media": [{ "field": "duration_s", "a": 92.4, "b": 88.0 }],
"sidecars": [{ "namespace": "ai.tags", "change": "changed", "fields": ["tags"] }],
"renditions": { "ver_A…": ["thumb", "proxy"], "ver_B…": ["thumb"] }
}shared_ratio close to 1 means a metadata-only re-save or a trim; close to 0 means new footage.
To see a whole folder as it was at a moment, or put it back, see History and rewind.
Change part of a file
Change a few bytes in the middle, insert, cut a range, append or truncate without sending the whole file. The result is saved as a new version. Only the chunks near your changes are new, so this is quick even on large files.
curl -X POST -H "Authorization: Bearer $SIDESPACE_TOKEN" -H 'If-Match: "ver_…"' -H "Content-Type: application/json" \
"$SIDESPACE_URL/v1/spaces/$SPACE/nodes/$NODE/edits" \
-d '{"ops": [{"at": 120, "delete": 4, "text": "night"}, {"at": 2048, "text": "\nINT. HARBOR"}]}'await side.edit("Film A", "/Ep01/script.md", [{ at: 120, delete: 4, text: "night" }], { ifVersionId: "ver_…" });
await side.appendText("Film A", "/Ep01/log.txt", "09:41 pick-ups done\n");side.edit("Film A", "/Ep01/script.md", [{"at": 120, "delete": 4, "text": "night"}], if_version_id="ver_…")
side.append_text("Film A", "/Ep01/log.txt", "09:41 pick-ups done\n")- Each op replaces
deletebytes starting atatwithtext(orbase64bytes).delete: 0inserts, no data deletes, andatequal to the file's size appends.truncatecuts the result to that many bytes last. - Offsets count in the version you edit, before any of your changes, and ranges must not overlap. A request takes at most 10,000 ops and 4 MiB of new data.
If-Match(orif_version_idin the body) names the version you read. If someone saved since, the answer is412and nothing changes, so you never edit on top of a save you haven't seen. Without it, the edit applies to the current version, and a save that lands while it runs still makes it fail.- Over MCP,
edit_textreplaces an exact piece of text andappend_textadds to the end; see MCP.
Sync and presence
GET /v1/spaces/{sid}/snapshotpages through every live node for a first sync. Then follow the journal from the snapshot'sat_seq.POST /v1/spaces/{sid}/presencewith{"node_id", "state": "viewing" | "editing" | "none"}tells others what you have open. Reports last 90 seconds unless renewed.GETlists them, and the realtime socket pushes them.