Skip to content

Upload sessions

Large files never travel through the control plane. An upload session brackets a batch of objects: the API authorizes them, your client PUTs the bytes straight to object storage, and finalize promotes them into the repository.

POST …/uploads → session
POST …/uploads/{sessionID}/objects → presigned PUT per object
PUT <upload_url> → your client → object storage
POST …/uploads/{sessionID}/objects/{oid}/complete
POST …/uploads/{sessionID}/finalize → promote, bump generation, emit events
Terminal window
export API=https://cavsnode.com/api/v1
export REPO=<repository-uuid>
export H="Authorization: Bearer $CAVS_TOKEN"
export J="Content-Type: application/json"

All five endpoints require repository.push (repo:write scope).

Open a session.

Terminal window
curl -sS -X POST "$API/repositories/$REPO/uploads" \
-H "$H" -H "$J" -H "Idempotency-Key: upload-$OID" \
-d '{"expected_objects": 3}'
Field Type Notes
expected_objects int Advisory count. Optional.

Headers: Idempotency-Key — strongly recommended. The same key returns the same session instead of creating a second one.

201 Created
{
"session": {
"id": "3b1f8a2c-…",
"repository_id": "0f8c…",
"expected_objects": 3,
"uploaded_objects": 0,
"uploaded_bytes": 0,
"status": "OPEN",
"created_at": "2026-07-28T12:00:00Z",
"expires_at": "2026-07-28T18:00:00Z"
}
}

Sessions live 6 hours. Emits upload.session.created.

POST /repositories/{repoID}/uploads/{sessionID}/objects

Section titled “POST /repositories/{repoID}/uploads/{sessionID}/objects”

Authorize objects and get a presigned PUT URL for each. Batch freely.

Terminal window
curl -sS -X POST "$API/repositories/$REPO/uploads/$SESSION/objects" \
-H "$H" -H "$J" \
-d '{"objects": [
{"oid": "9f2c8b0e…", "size": 104857600},
{"oid": "aa11bb22…", "size": 52428800}
]}'
Field Type Notes
objects[].oid string Lowercase hex SHA-256 of the object’s bytes, 64 chars
objects[].size int64 Exact byte length
200 OK
{
"objects": [
{
"oid": "9f2c8b0e…",
"upload_url": "https://<object-storage>/…?X-Amz-Signature=…",
"storage_key": "org/…/repo/…/objects/9f/2c/9f2c8b0e…",
"expires_in": 900
}
]
}

Send the raw bytes to object storage. The API is not in this path.

Terminal window
curl -sS --fail-with-body -X PUT "$URL" --upload-file ./model.safetensors
  • Set Content-Length (curl does).
  • No Authorization header — the signature is the credential.
  • Retriable: on a network error or 5xx, rewind and resend.
  • URLs expire in 15 minutes. If one lapses, re-request it from step 2.

POST /repositories/{repoID}/uploads/{sessionID}/objects/{oid}/complete

Section titled “POST /repositories/{repoID}/uploads/{sessionID}/objects/{oid}/complete”

Record that the object landed. This meters usage.

Terminal window
curl -sS -X POST \
"$API/repositories/$REPO/uploads/$SESSION/objects/$OID/complete" \
-H "$H" -H "$J" -d '{"size": 104857600}'

Returns 204 No Content. Idempotent.

POST /repositories/{repoID}/uploads/{sessionID}/finalize

Section titled “POST /repositories/{repoID}/uploads/{sessionID}/finalize”

Promote the objects into the repository, bump the generation, append to the usage ledger and emit events.

Terminal window
curl -sS -X POST "$API/repositories/$REPO/uploads/$SESSION/finalize" \
-H "$H" -H "Idempotency-Key: upload-$OID"
200 OK
{
"session": {
"id": "3b1f8a2c-…",
"status": "FINALIZED",
"uploaded_objects": 3,
"uploaded_bytes": 209715200
},
"generation": 42
}

Emits upload.session.finalized and repository.usage.changed, and audits repository.push.

Situation What to do
A PUT failed mid-transfer Rewind and re-PUT the same URL, or re-authorize if it expired
The process died after some objects landed Re-open with the same Idempotency-Key, re-authorize the remaining objects, complete, finalize
Finalize returned a network error Just call it again — it’s idempotent
A session was abandoned It expires in 6 hours and its objects are swept. Start fresh.
  1. Terminal window
    FILE=./model.safetensors
    OID=$(sha256sum "$FILE" | cut -d' ' -f1)
    SIZE=$(wc -c < "$FILE" | tr -d ' ')
    KEY="upload-$OID"
  2. Terminal window
    SESSION=$(curl -sS --fail-with-body -X POST "$API/repositories/$REPO/uploads" \
    -H "$H" -H "$J" -H "Idempotency-Key: $KEY" \
    -d '{"expected_objects": 1}' | jq -r '.session.id')
  3. Terminal window
    URL=$(curl -sS --fail-with-body -X POST \
    "$API/repositories/$REPO/uploads/$SESSION/objects" \
    -H "$H" -H "$J" \
    -d "{\"objects\":[{\"oid\":\"$OID\",\"size\":$SIZE}]}" \
    | jq -r '.objects[0].upload_url')
  4. Terminal window
    curl -sS --fail-with-body -X PUT "$URL" --upload-file "$FILE"
  5. Terminal window
    curl -sS --fail-with-body -X POST \
    "$API/repositories/$REPO/uploads/$SESSION/objects/$OID/complete" \
    -H "$H" -H "$J" -d "{\"size\": $SIZE}"
  6. Terminal window
    curl -sS --fail-with-body -X POST \
    "$API/repositories/$REPO/uploads/$SESSION/finalize" \
    -H "$H" -H "Idempotency-Key: $KEY"

Objects land at:

<storage_namespace>/objects/<oid[0:2]>/<oid[2:4]>/<oid>

The two-level shard keeps any single prefix from getting hot. Because the key derives from the content hash, writing the same bytes twice is the same key.

Limit Value
Session lifetime 6 hours
Presigned URL lifetime 15 minutes
Max object size plan-dependent: 5 GiB (Free) → 1 TiB (Business)
Concurrent uploads plan-dependent: 2 → 64
Request body 1 MiB — batch a few hundred objects per authorize call, not tens of thousands
API rate limit 30 req/s, burst 60