Git LFS endpoints
Two Git-facing data planes live under /repositories/{repoID}/lfs:
| Path | Used by | Transfers |
|---|---|---|
POST /lfs/objects/batch |
standard git-lfs |
whole objects, via presigned URLs |
GET/HEAD/PUT /lfs/* + POST /lfs/finalize |
cavs-lfs-agent |
chunks, through the API with Range |
export API=https://cavsnode.com/api/v1export REPO=<repository-uuid>export H="Authorization: Bearer $CAVS_TOKEN"The LFS base URL for a repository — what goes in lfs.url:
https://cavsnode.com/api/v1/repositories/{repoID}/lfsPOST /repositories/{repoID}/lfs/objects/batch
Section titled “POST /repositories/{repoID}/lfs/objects/batch”The standard Git LFS batch API
with the basic transfer adapter.
Permissions: the route guard requires repository.pull; an "operation": "upload" body additionally requires repository.push.
curl -sS -X POST "$API/repositories/$REPO/lfs/objects/batch" \ -H "$H" -H "Content-Type: application/json" \ -H "Accept: application/vnd.git-lfs+json" \ -d '{ "operation": "download", "transfers": ["basic"], "objects": [{"oid": "9f2c8b0e…", "size": 104857600}] }'{ "transfer": "basic", "objects": [ { "oid": "9f2c8b0e…", "size": 104857600, "actions": { "download": { "href": "https://<object-storage>/…?X-Amz-Signature=…", "expires_at": "2026-07-28T12:15:00Z" } } } ]}For "operation": "upload" each entry gets an upload action, plus a verify
action. Missing objects on download return a per-object
{"error": {"code": 404, "message": "object not found"}} inside the 200.
Objects land at <namespace>/objects/<xx>/<yy>/<oid> — the same layout
upload sessions use, so both paths share dedup on identical content.
Setup guide: Git LFS (standard).
The CAVS chunk tree
Section titled “The CAVS chunk tree”The transfer agent produces a content-addressed static export — asset manifests,
chunk maps and immutable packfiles — and reads it back with plain and ranged GETs.
These endpoints store and serve that tree.
GET /repositories/{repoID}/lfs/* serve a tree file (repository.pull)HEAD /repositories/{repoID}/lfs/* size probe (repository.pull)PUT /repositories/{repoID}/lfs/* store a tree file (repository.push)POST /repositories/{repoID}/lfs/finalize register + bump gen (repository.push)POST /repositories/{repoID}/lfs/transfers report a pull benchmark (repository.pull)Bytes live under <namespace>/cas/<relative path>, e.g.:
assets/<oid>/manifest.jsonchunks/packs/<hash>meta/…index.jsonGET / HEAD /repositories/{repoID}/lfs/*
Section titled “GET / HEAD /repositories/{repoID}/lfs/*”Serves a tree file. Honours Range and responds 206 Partial Content with
Content-Range when given one. Always sets Accept-Ranges: bytes.
curl -sS "$API/repositories/$REPO/lfs/assets/$OID/manifest.json" -H "$H"curl -sS -r 0-1023 "$API/repositories/$REPO/lfs/chunks/packs/$HASH" -H "$H"Relative paths containing an empty segment, . or .. are rejected with 400 before
a storage key is built.
PUT /repositories/{repoID}/lfs/*
Section titled “PUT /repositories/{repoID}/lfs/*”Stores a tree file. Returns 201 Created. The agent uses this to mirror the export
after a push.
POST /repositories/{repoID}/lfs/finalize
Section titled “POST /repositories/{repoID}/lfs/finalize”Registers the objects a push produced, bumps the generation and triggers usage reconciliation. This is the equivalent of an upload session’s finalize.
curl -sS -X POST "$API/repositories/$REPO/lfs/finalize" \ -H "$H" -H "Content-Type: application/json" \ -d '{ "objects": [ {"oid": "9f2c8b0e…", "size": 104857600, "physical": 3456789, "chunks": 412} ], "stats": { "kind": "push", "duration_ms": 8421, "ingest_ms": 5210, "mirror_ms": 2905, "logical_bytes": 104857600, "stored_bytes": 3456789, "object_count": 1, "chunk_count": 412, "new_chunks": 37, "reused_chunks": 375, "files_put": 44, "files_skipped": 368 } }'| Field | Notes |
|---|---|
objects[].oid |
Required. Entries with an empty oid are skipped. |
objects[].size |
Logical size |
objects[].physical |
Post-dedup, post-compression stored bytes. Optional (0 = not reported). |
objects[].chunks |
Chunk count. Optional. |
stats |
Optional session-level benchmark, recorded as a push transfer event |
{ "generation": 43, "objects": 1 }The manifest at <namespace>/cas/assets/<oid>/manifest.json is treated as the
object’s entry point. Emits repository.usage.changed and audits
repository.push.
POST /repositories/{repoID}/lfs/transfers
Section titled “POST /repositories/{repoID}/lfs/transfers”Pulls have no finalize, so the agent reports its aggregated pull benchmark here at
terminate. Permission: repository.pull. Best-effort — a failure never breaks the
pull.
Read the records back with
GET /repositories/{repoID}/transfers?kind=pull.
Which path a repository uses
Section titled “Which path a repository uses”Determined entirely by client-side Git config:
# Standard git-lfs → the basic transfer adaptergit config lfs.url "$API/repositories/$REPO/lfs"
# CAVS agent → the chunk treegit config lfs.standalonetransferagent cavsgit config lfs.customtransfer.cavs.path cavs-lfs-agentgit config lfs.customtransfer.cavs.concurrent falseBoth work against the same repository and the same objects. You can switch at any time; nothing needs re-uploading, because everything is addressed by oid.
Authenticating git-lfs
Section titled “Authenticating git-lfs”git config credential.helper store# or, scoped to the CAVS origin so the token never reaches your Git host:git config --add http.https://cavsnode.com/.extraHeader "Authorization: Bearer $CAVS_TOKEN"The agent reads $CAVS_TOKEN, then the token stored by cav login.
- Git LFS (standard) — the setup guide.
- CAVS LFS agent — options and tuning.
- Git index ingest — the metadata side of a push.

