LFS API
Git LFS Batch API, verify callback, and file locking endpoints.
Overview
These endpoints implement the Git LFS Batch API specification. They are called automatically by the git lfs client — you typically don't need to call them directly.
All LFS endpoints use application/vnd.git-lfs+json content type and Basic authentication (same credentials as git push).
For a usage guide with git lfs commands, see Git LFS Guide.
Batch API
POST /:org/:repo.git/info/lfs/objects/batch
POST /:org/:namespace/:repo.git/info/lfs/objects/batchAuth: Basic auth. Upload requires write access, download requires read access. Public repos allow unauthenticated download.
Request
{
"operation": "upload",
"transfers": ["basic"],
"objects": [
{ "oid": "78579d983fa87c4f9c3f4f86a32b02ff312db25dd0ec1f5cb7853c28c19ed060", "size": 524288 }
],
"ref": { "name": "refs/heads/main" }
}| Field | Required | Description |
|---|---|---|
operation | Yes | "upload" or "download" |
transfers | No | Transfer adapters (only "basic" supported) |
objects | Yes | Array of objects with oid (64-char hex SHA-256) and size (bytes) |
ref | No | Branch reference for authorization context |
Upload Response 200
{
"transfer": "basic",
"objects": [
{
"oid": "78579d...",
"size": 524288,
"actions": {
"upload": {
"href": "https://{account}.r2.cloudflarestorage.com/coregit-lfs/{orgId}/{repoId}/lfs/{oid}?X-Amz-Signature=...",
"header": { "Content-Type": "application/octet-stream" },
"expires_in": 900
},
"verify": {
"href": "https://api.coregit.dev/{org}/{repo}.git/info/lfs/verify",
"header": { "Authorization": "Basic ..." },
"expires_in": 900
}
}
}
],
"hash_algo": "sha256"
}The client PUTs the file directly to the upload.href URL (presigned R2), then POSTs to verify.href to confirm.
If the object already exists, actions is omitted — the client skips the upload:
{ "oid": "78579d...", "size": 524288 }Download Response 200
{
"transfer": "basic",
"objects": [
{
"oid": "78579d...",
"size": 524288,
"actions": {
"download": {
"href": "https://{account}.r2.cloudflarestorage.com/coregit-lfs/...?X-Amz-Signature=...",
"expires_in": 3600
}
}
}
],
"hash_algo": "sha256"
}Per-Object Errors
Objects that fail validation are returned with an error instead of actions:
{
"oid": "78579d...",
"size": 524288,
"error": { "code": 422, "message": "File too large. Max 100 MB for free tier." }
}| Code | Meaning |
|---|---|
| 404 | Object not found (download only) |
| 422 | Invalid oid, invalid size, or file too large |
| 507 | Storage quota exceeded |
Request-Level Errors
| Code | Meaning |
|---|---|
| 400 | Invalid JSON or missing required fields |
| 401 | Missing or invalid credentials |
| 403 | Insufficient permissions (e.g. read-only token on upload) |
| 404 | Repository not found |
| 422 | Too many objects in batch (max 20 free, 100 usage) |
| 500 | LFS storage not configured |
Verify Callback
POST /:org/:repo.git/info/lfs/verify
POST /:org/:namespace/:repo.git/info/lfs/verifyAuth: Basic auth (write access required).
Called by the client after a successful upload. Confirms the object exists in R2 and the size matches.
{ "oid": "78579d983fa87c4f9c3f4f86a32b02ff312db25dd0ec1f5cb7853c28c19ed060", "size": 524288 }Response 200 (empty body) on success.
| Error | Meaning |
|---|---|
| 404 | Object not found in R2 (upload didn't complete) |
| 422 | Size mismatch between claimed and actual |
On success, the object is recorded in the database and storage/transfer usage is tracked.
Create Lock
POST /:org/:repo.git/info/lfs/locks
POST /:org/:namespace/:repo.git/info/lfs/locksAuth: Basic auth (write access required).
{
"path": "assets/logo.psd",
"ref": { "name": "refs/heads/main" }
}Response 201:
{
"lock": {
"id": "umoiA4_WhzEbgWoUKLaS4",
"path": "assets/logo.psd",
"locked_at": "2026-04-06T12:00:00.000Z",
"owner": { "name": "VKi4S4wvJQmZP23FxV6s6" }
}
}The owner.name is the API key ID that created the lock.
409 if already locked:
{
"lock": { "id": "...", "path": "assets/logo.psd", "locked_at": "...", "owner": { "name": "..." } },
"message": "already created lock"
}List Locks
GET /:org/:repo.git/info/lfs/locks
GET /:org/:namespace/:repo.git/info/lfs/locksAuth: Basic auth (read access required).
Query parameters:
| Param | Description |
|---|---|
path | Filter by file path |
id | Filter by lock ID |
limit | Max results (1-100, default 100) |
cursor | Pagination cursor |
Response 200:
{
"locks": [
{
"id": "umoiA4_WhzEbgWoUKLaS4",
"path": "assets/logo.psd",
"locked_at": "2026-04-06T12:00:00.000Z",
"owner": { "name": "VKi4S4wvJQmZP23FxV6s6" }
}
],
"next_cursor": ""
}Verify Locks
POST /:org/:repo.git/info/lfs/locks/verify
POST /:org/:namespace/:repo.git/info/lfs/locks/verifyAuth: Basic auth (write access required).
{
"ref": { "name": "refs/heads/main" },
"limit": 100
}Response 200:
{
"ours": [
{ "id": "...", "path": "assets/logo.psd", "locked_at": "...", "owner": { "name": "..." } }
],
"theirs": [
{ "id": "...", "path": "data/model.bin", "locked_at": "...", "owner": { "name": "..." } }
],
"next_cursor": ""
}ours = locks owned by the current API key. theirs = locks owned by others. Git LFS blocks push if modified files appear in theirs.
Unlock
POST /:org/:repo.git/info/lfs/locks/:id/unlock
POST /:org/:namespace/:repo.git/info/lfs/locks/:id/unlockAuth: Basic auth (write access required).
{ "force": false }| Field | Description |
|---|---|
force | Set true to delete another user's lock. Requires master API key. |
Response 200:
{
"lock": { "id": "...", "path": "assets/logo.psd", "locked_at": "...", "owner": { "name": "..." } }
}403 if trying to unlock another user's lock without force: true or without master key.
Limits
| Tier | Max File Size | Objects per Batch |
|---|---|---|
| Free | 100 MB | 20 |
| Usage | 2 GB | 100 |
| Enterprise | Custom | Custom |
LFS storage and transfer count toward existing org quotas. No separate LFS billing.
Presigned URL Details
- Upload URLs expire in 15 minutes (900 seconds)
- Download URLs expire in 1 hour (3600 seconds)
- URLs point directly to Cloudflare R2 — the Coregit Worker is not in the data path
- Each URL is signed for a specific object key and cannot be reused for other objects