Resource guide
Files are byte streams
Treat file reads and writes as HTTP byte transfers, not JSON wrappers. Preserve content length,
ETag, byte-range responses, cancellation, and error status through your client. Read content types
are inferred from known file extensions and otherwise remain application/octet-stream; the bytes
are always authoritative.
Address paths deliberately
Normalize workspace-relative paths with the contract path grammar. Encode dynamic URL segments, reject traversal, and do not infer whether a path is a file or folder from its extension.
const encodedPath = segments.map(encodeURIComponent).join('/')
const rangeHeaders = { range: 'bytes=0-65535' }The snippet illustrates safe path encoding and a bounded byte range; use the endpoint reference for the final public route template before shipping a client.
Separate read and write grants
Tools that index or preview content need files:read. Editors and uploaders need files:write.
Request both only when the workflow genuinely performs both roles.
Symlinks are entries, not their targets
Listings and stat report a symbolic link as its own entry: type: "symlink" with a target
string holding the link text verbatim. A link is listed whether or not its target exists — a
dangling link is a legal, visible entry, because workspaces mirror real development trees where
links routinely point at paths that come and go. Listings can therefore now contain a third
type value; clients that switch on entry types should treat unrecognized values as opaque
entries rather than failing the listing. For a link entry, size is the byte length of the
target text and mtime is the link's own modification time.
Addressing a link never silently follows it — only a content read does. A GET on a link path
serves the target's bytes when the target resolves inside the workspace, and a successful read
through a link is byte-for-byte a read of the target: same ETag, length, range, and conditional
behavior. When the target dangles or escapes the workspace, the read fails with 409 symlink_unresolvable carrying the link's target — never a silent fallback. Writes are stricter:
a PUT, mkdir, or directory listing addressed at a link path fails with 409 path_is_symlink
rather than acting through the link. Directory links in intermediate path segments resolve
normally while they stay inside the workspace; link discipline applies to the path's final
component.
Make structural changes explicit
Use the JSON action endpoint for directories, moves, copies, recursive deletion, and link creation. Collision overwrites and recursion are opt-in. Recursive work is bounded by the gateway before mutation, so clients should surface a limit error instead of retrying an unsafe operation with broader local semantics.
Create or retarget a link:
{
"operation": "symlink",
"path": "current",
"target": "releases/v2"
}target is stored verbatim — relative targets are the portable choice, and targets outside the
workspace are legal to store but never readable through the link. symlink replaces an existing
symlink at path (that is how you retarget) and conflicts with any other existing entry type;
replacing a file with a link — or a link with a file — is always an explicit delete followed by a
create. This mirrors how PUT overwrites files in place: same-type replacement is direct,
cross-type replacement is deliberate.
Links participate in structural operations as ordinary entries. A copy reproduces a link as a link with the same verbatim target — it never follows one — so a relative target is reinterpreted at the destination and may dangle there; the same applies to moves. Deleting a link removes the link itself: a recursive delete of a directory containing a link-to-directory removes the link entry, never the target's tree.
Editors should retain the ETag returned by a read and send it in If-Match when saving. A 412
means the file changed after it was opened; reload it or require the user to deliberately replace
the newer version.
Files
6 operations /v1 /ws /{workspaceSlug} /files /{path}?list=1List one workspace directory with file stats
- Scope
ws:{workspaceSlug}:files:read- Request
- No JSON request body
- Response
Response JSON schema
{ "$schema": "https://json-schema.org/draft/2020-12/schema", "type": "object", "properties": { "entries": { "type": "array", "items": { "type": "object", "properties": { "name": { "type": "string", "minLength": 1 }, "path": { "type": "string" }, "type": { "type": "string", "enum": [ "file", "directory", "symlink" ] }, "size": { "type": "integer", "minimum": 0, "maximum": 9007199254740991 }, "mtime": { "type": "string", "format": "date-time", "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$" }, "target": { "type": "string" } }, "required": [ "name", "path", "type", "size", "mtime" ], "additionalProperties": false } } }, "required": [ "entries" ], "additionalProperties": false }- Errors
Symlink conflict (409)
{ "$schema": "https://json-schema.org/draft/2020-12/schema", "oneOf": [ { "type": "object", "properties": { "error": { "type": "string", "const": "symlink_unresolvable" }, "target": { "type": "string" } }, "required": [ "error", "target" ], "additionalProperties": false }, { "type": "object", "properties": { "error": { "type": "string", "const": "path_is_symlink" } }, "required": [ "error" ], "additionalProperties": false } ], "description": "Symlink conflict (409)" }- Delivery
- Standard response
- Retry
- Not declared idempotent
/v1 /ws /{workspaceSlug} /files /{path}?stat=1Read file or directory metadata without transferring file bytes
- Scope
ws:{workspaceSlug}:files:read- Request
- No JSON request body
- Response
Response JSON schema
{ "$schema": "https://json-schema.org/draft/2020-12/schema", "type": "object", "properties": { "entry": { "type": "object", "properties": { "name": { "type": "string", "minLength": 1 }, "path": { "type": "string" }, "type": { "type": "string", "enum": [ "file", "directory", "symlink" ] }, "size": { "type": "integer", "minimum": 0, "maximum": 9007199254740991 }, "mtime": { "type": "string", "format": "date-time", "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$" }, "target": { "type": "string" } }, "required": [ "name", "path", "type", "size", "mtime" ], "additionalProperties": false }, "etag": { "anyOf": [ { "type": "string" }, { "type": "null" } ] } }, "required": [ "entry", "etag" ], "additionalProperties": false }- Delivery
- Standard response
- Retry
- Not declared idempotent
/v1 /ws /{workspaceSlug} /files /{path}Stream one file with ETag and single-range support
- Scope
ws:{workspaceSlug}:files:read- Request
- No JSON request body
- Headers
Range— Request one byte range.If-None-Match— Return 304 when the current ETag matches.If-Modified-Since— Return 304 when the file is unchanged.
- Response
- Raw byte stream
- Errors
Symlink conflict (409)
{ "$schema": "https://json-schema.org/draft/2020-12/schema", "oneOf": [ { "type": "object", "properties": { "error": { "type": "string", "const": "symlink_unresolvable" }, "target": { "type": "string" } }, "required": [ "error", "target" ], "additionalProperties": false }, { "type": "object", "properties": { "error": { "type": "string", "const": "path_is_symlink" } }, "required": [ "error" ], "additionalProperties": false } ], "description": "Symlink conflict (409)" }- Delivery
- Standard response
- Retry
- Not declared idempotent
/v1 /ws /{workspaceSlug} /files /{path}Atomically write one file, optionally guarded by If-Match
- Scope
ws:{workspaceSlug}:files:write- Request
- Raw byte stream
- Headers
If-Match— Write only when the current ETag matches.If-Unmodified-Since— Write only when the file is unchanged.Content-Type— Preserve the uploaded media type.
- Response
Response JSON schema
{ "$schema": "https://json-schema.org/draft/2020-12/schema", "type": "object", "properties": { "ok": { "type": "boolean", "const": true }, "etag": { "type": "string" }, "size": { "type": "integer", "minimum": 0, "maximum": 9007199254740991 } }, "required": [ "ok", "etag", "size" ], "additionalProperties": false }- Errors
Error body JSON schema
{ "$schema": "https://json-schema.org/draft/2020-12/schema", "anyOf": [ { "oneOf": [ { "type": "object", "properties": { "error": { "type": "string", "const": "symlink_unresolvable" }, "target": { "type": "string" } }, "required": [ "error", "target" ], "additionalProperties": false }, { "type": "object", "properties": { "error": { "type": "string", "const": "path_is_symlink" } }, "required": [ "error" ], "additionalProperties": false } ], "description": "Symlink conflict (409)" }, { "type": "object", "properties": { "error": { "type": "string", "const": "storage_full" } }, "required": [ "error" ], "additionalProperties": false } ] }- Delivery
- Standard response
- Retry
- Not declared idempotent
/v1 /ws /{workspaceSlug} /files /{path}Delete one file or empty directory
- Scope
ws:{workspaceSlug}:files:write- Request
- No JSON request body
- Response
Response JSON schema
{ "$schema": "https://json-schema.org/draft/2020-12/schema", "type": "object", "properties": { "ok": { "type": "boolean", "const": true } }, "required": [ "ok" ], "additionalProperties": false }- Delivery
- Standard response
- Retry
- Declared idempotent
/v1 /ws /{workspaceSlug} /files /actionsCreate a directory or symlink, move, copy, or bounded-recursively delete
- Scope
ws:{workspaceSlug}:files:write- Request
Request JSON schema
{ "$schema": "https://json-schema.org/draft/2020-12/schema", "oneOf": [ { "type": "object", "properties": { "operation": { "type": "string", "const": "mkdir" }, "path": { "type": "string" }, "recursive": { "default": false, "type": "boolean" } }, "required": [ "operation", "path" ] }, { "type": "object", "properties": { "operation": { "type": "string", "const": "move" }, "source": { "type": "string" }, "destination": { "type": "string" }, "overwrite": { "default": false, "type": "boolean" } }, "required": [ "operation", "source", "destination" ] }, { "type": "object", "properties": { "operation": { "type": "string", "const": "copy" }, "source": { "type": "string" }, "destination": { "type": "string" }, "recursive": { "default": false, "type": "boolean" }, "overwrite": { "default": false, "type": "boolean" } }, "required": [ "operation", "source", "destination" ] }, { "type": "object", "properties": { "operation": { "type": "string", "const": "delete" }, "path": { "type": "string" }, "recursive": { "default": false, "type": "boolean" } }, "required": [ "operation", "path" ] }, { "type": "object", "properties": { "operation": { "type": "string", "const": "symlink" }, "path": { "type": "string" }, "target": { "type": "string", "minLength": 1, "maxLength": 4096 } }, "required": [ "operation", "path", "target" ] } ] }- Response
Response JSON schema
{ "$schema": "https://json-schema.org/draft/2020-12/schema", "type": "object", "properties": { "ok": { "type": "boolean", "const": true }, "affectedEntries": { "type": "integer", "exclusiveMinimum": 0, "maximum": 9007199254740991 } }, "required": [ "ok" ], "additionalProperties": false }- Errors
Error body JSON schema
{ "$schema": "https://json-schema.org/draft/2020-12/schema", "anyOf": [ { "oneOf": [ { "type": "object", "properties": { "error": { "type": "string", "const": "symlink_unresolvable" }, "target": { "type": "string" } }, "required": [ "error", "target" ], "additionalProperties": false }, { "type": "object", "properties": { "error": { "type": "string", "const": "path_is_symlink" } }, "required": [ "error" ], "additionalProperties": false } ], "description": "Symlink conflict (409)" }, { "type": "object", "properties": { "error": { "type": "string", "const": "storage_full" } }, "required": [ "error" ], "additionalProperties": false } ] }- Delivery
- Standard response
- Retry
- Not declared idempotent