#HTTP operation reference
Status: published operation reference
For searchable schemas, request examples, and generated client snippets, open the interactive Scalar API reference.
#Common request rules
Base origin: https://api.unicas.work
Every route is scoped by both appId and spaceId:
/v1/apps/{appId}/spaces/{spaceId}/...
Send a Space capability through the HTTP bearer authentication scheme:
Authorization: Bearer CAPABILITY
The HTTP API and Space capability claim both use family-local version 1: the signed
spaceId must match the route and permissions must contain the operation's
exact authority.
appId and spaceId are non-empty strings. A node hash is exactly 64
lowercase hexadecimal characters. JSON requests use application/json.
Canonical node bytes use application/vnd.unidocs.cas-node.v1.
The generated OpenAPI declares the bearer JWT security scheme globally rather
than modeling Authorization as an ordinary operation header.
#Operation inventory
| Client operation | Method and path | Authority | Success |
|---|---|---|---|
readContent |
GET /v1/apps/{appId}/spaces/{spaceId}/cas/nodes/{hash}/content |
cas:nodes:read |
Streamed canonical bytes |
readMetadata |
GET /v1/apps/{appId}/spaces/{spaceId}/cas/nodes/{hash}/metadata |
cas:nodes:read |
Metadata and retention state |
leaseNode |
POST /v1/apps/{appId}/spaces/{spaceId}/cas/nodes/{hash}/lease |
cas:nodes:lease |
Ready lease or direct-upload instructions |
usage |
GET /v1/apps/{appId}/spaces/{spaceId}/cas/usage |
cas:usage:read |
Space accounting |
gc |
POST /v1/apps/{appId}/spaces/{spaceId}/cas/gc |
cas:gc:execute |
Bounded collection result |
listRootRefs |
GET /v1/apps/{appId}/spaces/{spaceId}/root-refs |
cas:root-refs:read + refDomain |
Revision-stable page |
updateRootRefs |
POST /v1/apps/{appId}/spaces/{spaceId}/root-refs |
cas:root-refs:update + refDomain |
Atomic commit result |
There are no other public v1 Space operations in the current generated OpenAPI.
#Read node content
GET /v1/apps/APP_ID/spaces/SPACE_ID/cas/nodes/HASH/content
Authorization: Bearer CAPABILITY
Range: bytes=0-1023
The Range header is optional. The runtime accepts one standard byte range:
bounded (bytes=2-5), open-ended (bytes=7-), or suffix (bytes=-3).
Overshooting end positions are clamped to the object size.
- Full reads return
200with a streamed body. - Satisfiable ranges return
206with the selected bytes. - Invalid or unsatisfiable ranges return
416andContent-Range: bytes */{size}.
Both successful forms return Accept-Ranges: bytes, Content-Length, and the
node's Content-Type. A 206 also returns
Content-Range: bytes {start}-{end}/{size}. This path does not emit an
ETag. The 416 JSON body is:
{
"error": "INVALID_REQUEST",
"message": "Range is not satisfiable"
}
The public client exposes
readContent(hash, { offset, length }?, { signal }?). A zero-length client
range returns an empty stream without making a request. Range offset and length
must be non-negative safe integers.
This read is repeatable. Use bounded retries only for transient failures and honor cancellation.
#Read node metadata
GET /v1/apps/APP_ID/spaces/SPACE_ID/cas/nodes/HASH/metadata
Authorization: Bearer CAPABILITY
200 response:
{
"metadata": {
"hash": "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef",
"size": 1024,
"contentType": "application/vnd.unidocs.cas-node.v1",
"refs": []
},
"state": {
"leaseStartedAt": 1760000000000,
"leaseExpiresAt": 1760000900000,
"childRefCount": 0,
"rootRefCount": 1
}
}
size, lease timestamps, and childRefCount are non-negative integers.
rootRefCount is an integer. The public client returns the metadata projection
and may use configured metadata caching.
#Lease or upload a node
POST /v1/apps/APP_ID/spaces/SPACE_ID/cas/nodes/HASH/lease
Authorization: Bearer CAPABILITY
content-type: application/json
{"leaseDurationMs":900000}
The JSON property is required. Published clients use 15 minutes when the caller omits the complete options argument; the service clamps accepted values to 60 seconds through 24 hours.
Ready result:
{
"state": "ready",
"hash": "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef",
"leaseStartedAt": 1760000000000,
"leaseExpiresAt": 1760000900000
}
Upload result:
{
"state": "awaiting_upload",
"hash": "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef",
"upload": {
"method": "PUT",
"url": "https://UPLOAD_TARGET",
"expiresAt": 1760000300000,
"headers": {
"content-type": "application/vnd.unidocs.cas-node.v1",
"if-none-match": "*"
}
}
}
Use the returned method, URL, and headers exactly, then repeat the same lease
request. awaiting_replacement_upload additionally returns a required
rejection; validated_awaiting_children returns every distinct unready child
hash in canonical order and requires no second parent upload.
The service validates canonical bytes against HASH. Ready-node calls renew
the lease without re-uploading, parsing, or hashing.
Creating a new upload generation can return 429 CAS_UPLOAD_LIMIT when the
Space has too many active uploads. No generation is created for that request;
retry the same lease after active upload work has drained.
#Get Space usage
GET /v1/apps/APP_ID/spaces/SPACE_ID/cas/usage
Authorization: Bearer CAPABILITY
200 response:
{
"nodeCount": 12,
"readyContentBytes": 8192,
"readyStoredBytes": 6144,
"reservedBytes": 1024,
"notReadyNodeCount": 1,
"leasedNodeCount": 3
}
Every field is a non-negative integer. This is current operational accounting, not a business root catalog or billing contract. The GET is repeatable, but its values may change concurrently.
#Run bounded garbage collection
POST /v1/apps/APP_ID/spaces/SPACE_ID/cas/gc
Authorization: Bearer CAPABILITY
Content-Type: application/json
{ "maxNodes": 100 }
The body is optional. maxNodes, when present, is a positive integer. The
service default is 100.
200 response:
{
"examined": 100,
"deleted": 8,
"reclaimedContentBytes": 4096
}
A pass is bounded and race-safe. Each candidate is rechecked immediately before deletion; a node that became referenced or leased is skipped. Content is deleted before its metadata deletion is committed. A storage failure aborts without reporting that candidate as deleted.
The operation is not a promise to remove every eligible node. Additional bounded calls are safe as maintenance, but no idempotency key or automatic client retry is defined.
#List Root Refs
GET /v1/apps/APP_ID/spaces/SPACE_ID/root-refs?limit=100&cursor=CURSOR
Authorization: Bearer CAPABILITY
Query:
| Name | Constraint |
|---|---|
limit |
Optional integer from 1 through 200 |
cursor |
Optional non-empty opaque string |
200 response:
{
"refDomain": "files:primary",
"revision": 42,
"items": [
{
"hash": "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef",
"refCount": 1
}
],
"nextCursor": null
}
The service selects the Root Ref domain from the verified capability, never
from query or body data. The capability must contain a valid refDomain.
nextCursor is either a non-empty opaque string or null; continue with the
same capability domain. Pages are revision-stable.
#Atomically update Root Refs
POST /v1/apps/APP_ID/spaces/SPACE_ID/root-refs
Authorization: Bearer CAPABILITY
Content-Type: application/json
{
"requestId": "commit-018",
"changes": {
"0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef": 1
}
}
requestId is a non-empty stable idempotency identity. changes maps
lowercase SHA-256 hashes to signed integer deltas. The runtime additionally
requires non-empty changes, rejects duplicate JSON keys, caps the map at 1000
entries, and bounds each delta to 1,000,000 in magnitude.
200 response:
{
"success": true,
"idempotent": false,
"revision": 43
}
All deltas commit atomically within the capability's refDomain. A replay of
the exact request returns the original revision with idempotent: true. Reuse
of requestId with a different canonical payload returns
409 IDEMPOTENCY_CONFLICT. Missing nodes, unready positive targets, negative
aggregate balances, overflow, or an exhausted internal revision retry produce
an error and no partial commit.
After a lost response, resend exactly the same request and requestId.
#Error envelope
The shared contract and generated OpenAPI document:
{
"error": "ERROR_CODE",
"message": "Optional safe detail"
}
Only error is required by OpenAPI. The shared operation error map is:
| Status | Contract error |
|---|---|
400 |
INVALID_REQUEST |
401 |
UNAUTHORIZED |
403 |
FORBIDDEN |
404 |
NOT_FOUND |
409 |
CONFLICT |
413 |
PAYLOAD_TOO_LARGE |
429 |
RESOURCE_EXHAUSTED |
503 |
SERVICE_UNAVAILABLE |
The service returns more specific stable authorization codes such as
missing_token, invalid_token, unknown_issuer, registry_unavailable,
unsupported_algorithm, APP_SUSPENDED, resource_scope_mismatch, and
insufficient_permission. Upload and Root Ref validation likewise use specific
codes described above. Callers should branch on the stable error code and
treat message as optional diagnostic text.
#Contract gaps
The current sources have known representational gaps:
- The TypeScript contract models
readContentasReadableStream<Uint8Array>using the canonical media type, while its generated OpenAPI200response has emptycontent. - Runtime and the public client support byte ranges, but the TypeScript
operation inputs and generated OpenAPI do not declare
Range,206,416, or response headers. - OpenAPI declares a bearer JWT scheme and says the Space and permission must match, but capability claims and per-operation permissions are descriptive, not machine-modeled OpenAPI scopes.
- Runtime validation is intentionally stricter in several workflows, including inline upload length, Root Ref canonicalization and bounds, and specific error codes.
Integrators must not infer new routes or fields from these gaps. The generated OpenAPI remains the operation inventory, and runtime-only behavior above is supported by service tests.