Downloads
Downloading is two steps: authorize a set of oids, then GET the presigned URLs you
get back.
export API=https://cavsnode.com/api/v1export REPO=<repository-uuid>export H="Authorization: Bearer $CAVS_TOKEN"POST /repositories/{repoID}/downloads/authorize
Section titled “POST /repositories/{repoID}/downloads/authorize”Permission: repository.pull (repo:read scope).
curl -sS -X POST "$API/repositories/$REPO/downloads/authorize" \ -H "$H" -H "Content-Type: application/json" \ -d '{"oids": ["9f2c8b0e…", "aa11bb22…", "deadbeef…"]}'| Field | Type | Notes |
|---|---|---|
oids |
string[] | Lowercase hex SHA-256 values. Batch as many as fit in a 1 MiB body. |
{ "objects": [ { "oid": "9f2c8b0e…", "size": 104857600, "download_url": "https://<object-storage>/…?X-Amz-Signature=…", "expires_in": 900 }, { "oid": "aa11bb22…", "size": 52428800, "download_url": "https://…", "expires_in": 900 }, { "oid": "deadbeef…", "error": "not found" } ]}Fetching the bytes
Section titled “Fetching the bytes”curl -sS --fail-with-body -o model.safetensors.part "$URL"echo "$OID model.safetensors.part" | sha256sum -c -mv model.safetensors.part model.safetensorsPresigned URLs support Range, so you can resume:
curl -sS -C - -o model.safetensors.part "$URL" # resume from where it stoppedThey carry no Authorization header — the signature is the credential — and expire in
15 minutes.
Side effects
Section titled “Side effects”Authorizing a download:
- Increments each served object’s download counter (visible in
GET …/objectsand the artifact detail view). - Records egress against the organization’s usage — for managed storage only; BYOS egress is your provider’s bill.
- Appends to the usage ledger with the acting user, object count and byte total.
Batching
Section titled “Batching”One call for many oids is much better than many calls:
# Authorize every object in a release, in one requestjq -n --argjson oids "$(cat oids.json)" '{oids: $oids}' \ | curl -sS -X POST "$API/repositories/$REPO/downloads/authorize" \ -H "$H" -H "Content-Type: application/json" -d @- \ | jq -r '.objects[] | select(.download_url) | "\(.oid)\t\(.download_url)"' \ | while IFS=$'\t' read -r oid url; do curl -sS --fail-with-body -o "$oid.part" "$url" echo "$oid $oid.part" | sha256sum -c - && mv "$oid.part" "$oid" doneThe request body cap is 1 MiB, so a batch of a few thousand oids is fine. Respect the 30 req/s API limit — but note the transfers themselves run against object storage and don’t consume your API budget.
Checking existence without downloading
Section titled “Checking existence without downloading”Authorization is cheap and tells you what’s there. This is how cav verify works:
curl -sS -X POST "$API/repositories/$REPO/downloads/authorize" \ -H "$H" -H "Content-Type: application/json" \ -d "{\"oids\":[\"$OID\"]}" \ | jq -e '.objects[0].download_url != null' >/dev/null \ && echo present || echo missingOr cav verify ./file — it hashes locally and asks the same question.
The other download paths
Section titled “The other download paths”| Path | Use |
|---|---|
| This endpoint | SDKs, CLI, CI, custom clients |
POST …/lfs/objects/batch |
Standard git-lfs |
GET …/lfs/* |
The CAVS transfer agent’s chunk-level fetch, with Range |
The chunk-tree path is the only one that transfers less than the whole object — it fetches only the chunks the local cache lacks. See CAVS LFS agent.
Client checklist
Section titled “Client checklist”- Check the per-entry
errorfield before touchingdownload_url. - Stream to a temp path; never write directly to the destination.
- Hash while streaming; compare against the oid; delete on mismatch.
- Rename atomically only after verification passes.
- Re-request an expired URL rather than caching it.
- Retry
5xxand network errors with backoff; don’t retry403from storage (the URL expired — re-authorize instead).
- Upload sessions — the other direction.
- Git LFS endpoints — the Git-native paths.
- Plain HTTP — a complete verified-download implementation.

