Plain HTTP
No SDK for your language? The whole data plane is four calls to upload and one to download. This page is the complete recipe.
export API=https://cavsnode.com/api/v1export TOKEN=cavs_ci_...export REPO=<repository-uuid>export H_AUTH="Authorization: Bearer $TOKEN"export H_JSON="Content-Type: application/json"Get the repository UUID from the dashboard URL, or:
curl -sS "$API/organizations/acme-ai/repositories" -H "$H_AUTH" \ | jq -r '.repositories[] | "\(.slug)\t\(.id)"'Set your telemetry headers too — it’s how you’ll be identifiable when something goes wrong:
export H_CLIENT="X-CAVS-Client: generic-http"export H_INTEG="X-CAVS-Integration: generic-http"Upload
Section titled “Upload”-
Hash the file. The oid is the SHA-256 — lowercase hex, 64 characters.
Terminal window FILE=./model.safetensorsOID=$(sha256sum "$FILE" | cut -d' ' -f1) # shasum -a 256 on macOSSIZE=$(wc -c < "$FILE" | tr -d ' ')Hash by streaming. Never load the whole file into memory.
-
Open an upload session. Send an
Idempotency-Keyso a retry resumes the same session instead of creating a second one.Terminal window SESSION=$(curl -sS --fail-with-body -X POST "$API/repositories/$REPO/uploads" \-H "$H_AUTH" -H "$H_JSON" -H "Idempotency-Key: upload-$OID" \-d '{"expected_objects": 1}' | jq -r '.session.id')201 Created {"session": {"id": "3b1f8a2c-…","repository_id": "0f8c…","expected_objects": 1,"uploaded_objects": 0,"uploaded_bytes": 0,"status": "OPEN","created_at": "2026-07-28T12:00:00Z"}} -
Authorize the objects and get a presigned
PUTURL for each. Batch as many as you like in one call.Terminal window RESP=$(curl -sS --fail-with-body -X POST \"$API/repositories/$REPO/uploads/$SESSION/objects" \-H "$H_AUTH" -H "$H_JSON" \-d "{\"objects\":[{\"oid\":\"$OID\",\"size\":$SIZE}]}")URL=$(echo "$RESP" | jq -r '.objects[0].upload_url')200 OK {"objects": [{"oid": "9f2c8b0e…","upload_url": "https://<object-storage>/…?X-Amz-Signature=…","storage_key": "org/…/repo/…/objects/9f/2c/9f2c8b0e…","expires_in": 900}]}This is where quota enforcement happens: a batch that would exceed your plan’s storage or object-size limit is refused here with
402, before anything is written. -
PUTthe bytes straight to object storage. The API is not in this path.Terminal window curl -sS --fail-with-body -X PUT "$URL" --upload-file "$FILE"Send
Content-Length(curl does this for you). Retriable: on a network error or5xx, rewind and resend. -
Mark it complete, which records the object and meters usage.
Terminal window curl -sS --fail-with-body -X POST \"$API/repositories/$REPO/uploads/$SESSION/objects/$OID/complete" \-H "$H_AUTH" -H "$H_JSON" -d "{\"size\": $SIZE}"Returns
204 No Content. -
Finalize the session. This promotes the objects, bumps the repository generation and emits events. Idempotent.
Terminal window curl -sS --fail-with-body -X POST \"$API/repositories/$REPO/uploads/$SESSION/finalize" \-H "$H_AUTH" -H "Idempotency-Key: upload-$OID"200 OK { "session": { "id": "3b1f…", "status": "FINALIZED", "uploaded_objects": 1 }, "generation": 42 }
Download
Section titled “Download”DL=$(curl -sS --fail-with-body -X POST \ "$API/repositories/$REPO/downloads/authorize" \ -H "$H_AUTH" -H "$H_JSON" \ -d "{\"oids\":[\"$OID\"]}")
URL=$(echo "$DL" | jq -r '.objects[0].download_url')
curl -sS --fail-with-body -o restored.bin.part "$URL"echo "$OID restored.bin.part" | sha256sum -c - # verify BEFORE renamingmv restored.bin.part restored.bin{ "objects": [ { "oid": "9f2c8b0e…", "size": 104857600, "download_url": "https://<object-storage>/…", "expires_in": 900 }, { "oid": "aa11bb22…", "error": "not found" } ]}Note that a missing object comes back as a per-oid error field inside a 200,
not as a 404. Check each entry.
A complete Go implementation
Section titled “A complete Go implementation”package cavs
import ( "bytes" "crypto/sha256" "encoding/hex" "encoding/json" "fmt" "io" "net/http" "os" "path/filepath")
type Client struct { API string // https://cavsnode.com/api/v1 Token string Repo string // repository UUID HTTP *http.Client}
func (c *Client) do(method, path string, body any, out any, headers map[string]string) error { var r io.Reader if body != nil { b, err := json.Marshal(body) if err != nil { return err } r = bytes.NewReader(b) } req, err := http.NewRequest(method, c.API+path, r) if err != nil { return err } req.Header.Set("Authorization", "Bearer "+c.Token) req.Header.Set("X-CAVS-Client", "go-generic") req.Header.Set("X-CAVS-Integration", "generic-http") if body != nil { req.Header.Set("Content-Type", "application/json") } for k, v := range headers { req.Header.Set(k, v) }
res, err := c.HTTP.Do(req) if err != nil { return err } defer res.Body.Close()
if res.StatusCode >= 300 { b, _ := io.ReadAll(io.LimitReader(res.Body, 1<<16)) return fmt.Errorf("%s %s: %s: %s", method, path, res.Status, b) } if out != nil && res.StatusCode != http.StatusNoContent { return json.NewDecoder(res.Body).Decode(out) } return nil}
// hashFile streams the file and returns its oid and size.func hashFile(path string) (string, int64, error) { f, err := os.Open(path) if err != nil { return "", 0, err } defer f.Close()
h := sha256.New() n, err := io.Copy(h, f) if err != nil { return "", 0, err } return hex.EncodeToString(h.Sum(nil)), n, nil}
// Upload runs the whole choreography and returns the object's oid.func (c *Client) Upload(path, idempotencyKey string) (string, error) { oid, size, err := hashFile(path) if err != nil { return "", err } idem := map[string]string{"Idempotency-Key": idempotencyKey}
var session struct { Session struct{ ID string } `json:"session"` } if err := c.do("POST", "/repositories/"+c.Repo+"/uploads", map[string]int{"expected_objects": 1}, &session, idem); err != nil { return "", err } sid := session.Session.ID
var auth struct { Objects []struct { OID string `json:"oid"` UploadURL string `json:"upload_url"` } `json:"objects"` } if err := c.do("POST", "/repositories/"+c.Repo+"/uploads/"+sid+"/objects", map[string]any{"objects": []map[string]any{{"oid": oid, "size": size}}}, &auth, nil); err != nil { return "", err } if len(auth.Objects) == 0 || auth.Objects[0].UploadURL == "" { return "", fmt.Errorf("no upload url returned for %s", oid) }
// PUT the bytes directly to object storage. f, err := os.Open(path) if err != nil { return "", err } defer f.Close()
put, err := http.NewRequest("PUT", auth.Objects[0].UploadURL, f) if err != nil { return "", err } put.ContentLength = size res, err := c.HTTP.Do(put) if err != nil { return "", err } defer res.Body.Close() if res.StatusCode >= 300 { return "", fmt.Errorf("PUT object: %s", res.Status) }
if err := c.do("POST", "/repositories/"+c.Repo+"/uploads/"+sid+"/objects/"+oid+"/complete", map[string]int64{"size": size}, nil, nil); err != nil { return "", err } if err := c.do("POST", "/repositories/"+c.Repo+"/uploads/"+sid+"/finalize", nil, nil, idem); err != nil { return "", err } return oid, nil}
// Download fetches one object, verifying its hash before committing the file.func (c *Client) Download(oid, dest string) error { var auth struct { Objects []struct { OID string `json:"oid"` DownloadURL string `json:"download_url"` Error string `json:"error"` } `json:"objects"` } if err := c.do("POST", "/repositories/"+c.Repo+"/downloads/authorize", map[string][]string{"oids": {oid}}, &auth, nil); err != nil { return err } if len(auth.Objects) == 0 || auth.Objects[0].Error != "" { return fmt.Errorf("authorize %s: %s", oid, auth.Objects[0].Error) }
res, err := c.HTTP.Get(auth.Objects[0].DownloadURL) if err != nil { return err } defer res.Body.Close() if res.StatusCode >= 300 { return fmt.Errorf("GET object: %s", res.Status) }
tmp := dest + ".part" f, err := os.Create(tmp) if err != nil { return err } h := sha256.New() if _, err := io.Copy(io.MultiWriter(f, h), res.Body); err != nil { f.Close() os.Remove(tmp) return err } if err := f.Close(); err != nil { os.Remove(tmp) return err }
if got := hex.EncodeToString(h.Sum(nil)); got != oid { os.Remove(tmp) return fmt.Errorf("checksum mismatch: got %s, want %s", got, oid) } if err := os.MkdirAll(filepath.Dir(dest), 0o755); err != nil { return err } return os.Rename(tmp, dest)}Implementation checklist
Section titled “Implementation checklist”Write your own client and these are the things that bite.
Must
- Stream the hash; never buffer a whole file.
- Send
Idempotency-Keyon session create and finalize. - Verify SHA-256 == oid before renaming the temp file into place.
- Retry
429and5xxwith exponential backoff and jitter — 4 attempts is the convention. - Set connect and read timeouts, and cap redirects at 5.
- Check per-oid
errorfields inside200responses. - Set
X-CAVS-ClientandX-CAVS-Integration.
Must not
- Log tokens or presigned URLs.
- Persist presigned URLs — they expire in 15 minutes; just re-request.
- Assume finalize is optional.
- Write a file whose hash doesn’t match.
- Follow relative paths from a directory listing without guarding against traversal.
Errors
Section titled “Errors”{ "error": { "code": "quota_exceeded", "message": "storage quota exceeded", "request_id": "8c1f…" } }| Status | code |
Meaning |
|---|---|---|
| 400 | bad_request |
Malformed body, or an unknown JSON field — the API rejects those |
| 401 | unauthorized |
Missing, invalid or revoked credential |
| 403 | forbidden |
Role, override or scope insufficient |
| 404 | not_found |
No such resource |
| 409 | conflict |
State conflict, e.g. a duplicate slug |
| 402 | quota_exceeded |
A plan limit — see details |
| 429 | rate_limited |
30 req/s sustained, burst 60 |
| 500 | internal |
Quote request_id to support |
Full detail in Errors.
- Upload sessions — the endpoint reference.
- Downloads — the endpoint reference.
- CI/CD recipes — a reusable shell wrapper for this flow.

