Skip to content

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.

Terminal window
export API=https://cavsnode.com/api/v1
export 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:

Terminal window
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:

Terminal window
export H_CLIENT="X-CAVS-Client: generic-http"
export H_INTEG="X-CAVS-Integration: generic-http"
  1. Hash the file. The oid is the SHA-256 — lowercase hex, 64 characters.

    Terminal window
    FILE=./model.safetensors
    OID=$(sha256sum "$FILE" | cut -d' ' -f1) # shasum -a 256 on macOS
    SIZE=$(wc -c < "$FILE" | tr -d ' ')

    Hash by streaming. Never load the whole file into memory.

  2. Open an upload session. Send an Idempotency-Key so 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"
    }
    }
  3. Authorize the objects and get a presigned PUT URL 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.

  4. PUT the 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 or 5xx, rewind and resend.

  5. 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.

  6. 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 }
Terminal window
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 renaming
mv restored.bin.part restored.bin
200 OK — one entry per requested oid
{
"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.

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)
}

Write your own client and these are the things that bite.

Must

  • Stream the hash; never buffer a whole file.
  • Send Idempotency-Key on session create and finalize.
  • Verify SHA-256 == oid before renaming the temp file into place.
  • Retry 429 and 5xx with exponential backoff and jitter — 4 attempts is the convention.
  • Set connect and read timeouts, and cap redirects at 5.
  • Check per-oid error fields inside 200 responses.
  • Set X-CAVS-Client and X-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.
{ "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.