Skip to content

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

POST /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.

Terminal window
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}]
}'
200 OK — Content-Type: application/vnd.git-lfs+json
{
"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 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.json
chunks/packs/<hash>
meta/…
index.json

Serves a tree file. Honours Range and responds 206 Partial Content with Content-Range when given one. Always sets Accept-Ranges: bytes.

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

Stores a tree file. Returns 201 Created. The agent uses this to mirror the export after a push.

Registers the objects a push produced, bumps the generation and triggers usage reconciliation. This is the equivalent of an upload session’s finalize.

Terminal window
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
200 OK
{ "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.

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.

Determined entirely by client-side Git config:

Terminal window
# Standard git-lfs → the basic transfer adapter
git config lfs.url "$API/repositories/$REPO/lfs"
# CAVS agent → the chunk tree
git config lfs.standalonetransferagent cavs
git config lfs.customtransfer.cavs.path cavs-lfs-agent
git config lfs.customtransfer.cavs.concurrent false

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

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