DocumentationAPI Reference

REST API Reference

The V1 API powers the desktop app and supports custom integrations. Every endpoint includes curl, TypeScript, and JSON examples you can copy directly. This page covers the core file lifecycle; branches, shared plugins, draft commits, and batch download URLs are also exposed on V1 and documented on request.

Authentication

All API requests require a project key sent via the Authorization header. Keys use the usc_ prefix and can be generated from the web dashboard under project settings.

Base URL

https://usourcecontrol.com

Auth header

Bearer usc_...

Content type

application/json
Examplebash
curl -X POST https://usourcecontrol.com/api/v1/auth/validate-key \
  -H "Authorization: Bearer usc_your_project_key" \
  -H "Content-Type: application/json"

Conventions

  • Every identifier (project, file, commit, user, org) is a UUID.
  • A key authenticates one project. Child resources are re-scoped to that project server-side, so a file ID from another project returns 404, never data.
  • Keys bind to the first device that uses them. A key replayed from elsewhere gets 403 with code: "device_mismatch".
  • List endpoints are keyset-paginated: { data, nextCursor }. Page with ?cursor= until nextCursor is null. Default page size 50, max 200.
  • Errors return { message } with a meaningful status. Writes are rate limited per project and answer 429 when you exceed it.

Endpoints

POST/api/v1/auth/validate-key

Validate project key

Authenticate a project key and receive the caller's identity plus the project and organization it is scoped to. Fields are returned flat, not nested. Project keys are locked to the first device that uses them, so a key used from anywhere else is rejected with 403 and a `code` of device_mismatch.

curlbash
curl -X POST https://usourcecontrol.com/api/v1/auth/validate-key \
  -H "Authorization: Bearer usc_your_project_key" \
  -H "Content-Type: application/json"
Request body
No body. Key is sent via Authorization header.
Responsejson
{
  "valid": true,
  "userId": "3f1a0c52-9c1e-4f0a-9a1b-7d2e5c8b4a10",
  "userName": "Sarah Chen",
  "userAvatarUrl": null,
  "projectId": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
  "projectName": "MyGame",
  "projectSlug": "mygame",
  "projectIconUrl": null,
  "orgId": "1b9d6bcd-bbfd-4b2d-9b5d-ab8dfbbd4bed",
  "orgName": "My Studio",
  "orgSlug": "my-studio"
}
TypeScriptts
const res = await fetch("https://usourcecontrol.com/api/v1/auth/validate-key", {
  method: "POST",
  headers: {
    "Authorization": "Bearer usc_your_project_key",
    "Content-Type": "application/json",
  },
});
const auth = await res.json();
// On failure: { valid: false, code, message } with 401 or 403.
if (!auth.valid) throw new Error(auth.message);
console.log(auth.projectName, auth.projectId); // "MyGame" "7c9e6679-..."
POST/api/v1/projects/{projectId}/sync

Sync file status

Compare local files against the project's committed state. Send each local path with its SHA-256 as `sha256`. The server returns one status row per file — it does not return bucketed path arrays. Set `includeRemoteOnly` on the last batch to also receive files you don't have locally. Omit `branch` to sync against main.

curlbash
curl -X POST https://usourcecontrol.com/api/v1/projects/7c9e6679-7425-40de-944b-e07fc1f90ae7/sync \
  -H "Authorization: Bearer usc_your_project_key" \
  -H "Content-Type: application/json" \
  -d '{
    "files": [
      { "path": "Content/Maps/MainLevel.umap", "sha256": "a1b2c3..." },
      { "path": "Content/Characters/Hero.uasset", "sha256": "d4e5f6..." }
    ],
    "includeRemoteOnly": true
  }'
Request bodyjson
{
  "files": [
    { "path": "Content/Maps/MainLevel.umap", "sha256": "a1b2c3..." },
    { "path": "Content/Characters/Hero.uasset", "sha256": "d4e5f6..." }
  ],
  "includeRemoteOnly": true,
  "branch": "main"
}
Responsejson
{
  "files": [
    {
      "path": "Content/Maps/MainLevel.umap",
      "status": "modified",
      "fileId": "f47ac10b-58cc-4372-a567-0e02b2c3d479",
      "remoteVersion": 11,
      "remoteSha256": "9f8e7d...",
      "remoteCommitSeq": 24,
      "remoteSize": 524288000
    },
    {
      "path": "Content/Characters/Hero.uasset",
      "status": "up_to_date",
      "fileId": "2c4a1e88-1f3b-4a90-8c77-6b1d0e5a3f21",
      "remoteVersion": 3,
      "remoteSha256": "d4e5f6...",
      "remoteCommitSeq": 19,
      "remoteSize": 10485760
    }
  ],
  "latestCommitSeq": 24
}
TypeScriptts
const res = await fetch(`https://usourcecontrol.com/api/v1/projects/${projectId}/sync`, {
  method: "POST",
  headers: {
    "Authorization": `Bearer ${apiKey}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    files: localFiles.map(f => ({ path: f.path, sha256: f.sha256 })),
    includeRemoteOnly: true,
  }),
});
const { files, latestCommitSeq } = await res.json();

// status is one of: up_to_date | outdated | missing | local_only
//                 | modified | conflict | deleted_remote
const toUpload = files.filter(f => f.status === "modified" || f.status === "local_only");
const toDownload = files.filter(f => f.status === "outdated" || f.status === "missing");
POST/api/v1/projects/{projectId}/files/upload/initiate

Initiate a file upload

Reserve a file version and get a presigned PUT URL. `contentMd5` is required — it is signed into the URL alongside the byte length, so storage rejects any body that doesn't match exactly. Send the returned `requiredHeaders` verbatim on your PUT. Max file size: 5 GB (413 above that).

curlbash
curl -X POST https://usourcecontrol.com/api/v1/projects/7c9e6679-7425-40de-944b-e07fc1f90ae7/files/upload/initiate \
  -H "Authorization: Bearer usc_your_project_key" \
  -H "Content-Type: application/json" \
  -d '{
    "path": "Content/Maps/MainLevel.umap",
    "sha256": "a1b2c3...",
    "size": 524288000,
    "contentMd5": "rL0Y20zC+Fzt72VPzMSk2A=="
  }'
Request bodyjson
{
  "path": "Content/Maps/MainLevel.umap",
  "sha256": "a1b2c3...",
  "size": 524288000,
  "contentMd5": "rL0Y20zC+Fzt72VPzMSk2A=="
}
Responsejson
{
  "path": "Content/Maps/MainLevel.umap",
  "fileId": "f47ac10b-58cc-4372-a567-0e02b2c3d479",
  "version": 12,
  "storageKey": "1b9d6bcd/7c9e6679/f47ac10b/12/a1b2c3_MainLevel.umap",
  "uploadUrl": "https://s3.eu-central-003.backblazeb2.com/...",
  "requiredHeaders": {
    "Content-Length": "524288000",
    "Content-MD5": "rL0Y20zC+Fzt72VPzMSk2A=="
  }
}
TypeScriptts
// Step 1: reserve the version and get a presigned PUT
const initRes = await fetch(
  `https://usourcecontrol.com/api/v1/projects/${projectId}/files/upload/initiate`,
  {
    method: "POST",
    headers: { "Authorization": `Bearer ${apiKey}`, "Content-Type": "application/json" },
    body: JSON.stringify({ path, sha256, size, contentMd5 }),
  },
);
const { fileId, version, storageKey, uploadUrl, requiredHeaders } = await initRes.json();

// Step 2: PUT the bytes straight to storage with the signed headers
await fetch(uploadUrl, { method: "PUT", body: fileBuffer, headers: requiredHeaders });

// Step 3: confirm (see below) — then reference { fileId, version } in a commit.
POST/api/v1/projects/{projectId}/files/upload/initiate-batch

Initiate up to 1,000 uploads

The batched form of initiate, and what the desktop app uses. Up to 1,000 files per call. Results are per-file and isolated: a file whose content already exists in this project comes back as `{ dedup: true }` with no upload URL (skip the PUT, confirm with `dedup: true`), and a file that fails validation comes back as `{ path, error }` without sinking the batch.

curlbash
curl -X POST https://usourcecontrol.com/api/v1/projects/7c9e6679-7425-40de-944b-e07fc1f90ae7/files/upload/initiate-batch \
  -H "Authorization: Bearer usc_your_project_key" \
  -H "Content-Type: application/json" \
  -d '{
    "files": [
      { "path": "Content/FX/P_Smoke.uasset", "sha256": "aa11...", "size": 20480, "contentMd5": "..." },
      { "path": "Content/FX/P_Fire.uasset",  "sha256": "bb22...", "size": 30720, "contentMd5": "..." }
    ]
  }'
Request bodyjson
{
  "files": [
    { "path": "Content/FX/P_Smoke.uasset", "sha256": "aa11...", "size": 20480, "contentMd5": "..." },
    { "path": "Content/FX/P_Fire.uasset",  "sha256": "bb22...", "size": 30720, "contentMd5": "..." }
  ]
}
Responsejson
{
  "results": [
    {
      "path": "Content/FX/P_Smoke.uasset",
      "fileId": "f47ac10b-58cc-4372-a567-0e02b2c3d479",
      "version": 4,
      "storageKey": "1b9d6bcd/7c9e6679/f47ac10b/4/aa11_P_Smoke.uasset",
      "uploadUrl": "https://s3.eu-central-003.backblazeb2.com/...",
      "requiredHeaders": { "Content-Length": "20480", "Content-MD5": "..." }
    },
    {
      "path": "Content/FX/P_Fire.uasset",
      "fileId": "9c0f1a77-4e2b-4d18-b3aa-51c7e9d0b246",
      "version": 2,
      "dedup": true
    }
  ]
}
TypeScriptts
const res = await fetch(
  `https://usourcecontrol.com/api/v1/projects/${projectId}/files/upload/initiate-batch`,
  {
    method: "POST",
    headers: { "Authorization": `Bearer ${apiKey}`, "Content-Type": "application/json" },
    body: JSON.stringify({ files: batch }), // 1..1000 entries
  },
);
const { results } = await res.json();

for (const r of results) {
  if ("error" in r) continue;                  // per-file failure, batch survives
  if (!("dedup" in r)) {
    await fetch(r.uploadUrl, {
      method: "PUT",
      body: await read(r.path),
      headers: r.requiredHeaders,
    });
  }
  // Confirm either way — dedup files just skip the PUT.
}
POST/api/v1/projects/{projectId}/files/upload/confirm

Confirm an upload

Record the uploaded version. The server HEADs the object to prove it landed, then inserts the version row and advances the file's tip. Pass the `storageKey` you were given at initiate; for a deduped file omit it and send `dedup: true` instead. A 409 with `versionConflict` means another upload took that version — re-initiate to get a fresh one. A confirmed version is not visible to your team until you reference it in a commit.

curlbash
curl -X POST https://usourcecontrol.com/api/v1/projects/7c9e6679-7425-40de-944b-e07fc1f90ae7/files/upload/confirm \
  -H "Authorization: Bearer usc_your_project_key" \
  -H "Content-Type: application/json" \
  -d '{
    "fileId": "f47ac10b-58cc-4372-a567-0e02b2c3d479",
    "version": 12,
    "storageKey": "1b9d6bcd/7c9e6679/f47ac10b/12/a1b2c3_MainLevel.umap",
    "sha256": "a1b2c3...",
    "size": 524288000
  }'
Request bodyjson
{
  "fileId": "f47ac10b-58cc-4372-a567-0e02b2c3d479",
  "version": 12,
  "storageKey": "1b9d6bcd/7c9e6679/f47ac10b/12/a1b2c3_MainLevel.umap",
  "sha256": "a1b2c3...",
  "size": 524288000
}
Responsejson
{
  "confirmed": true,
  "version": 12
}
TypeScriptts
const res = await fetch(
  `https://usourcecontrol.com/api/v1/projects/${projectId}/files/upload/confirm`,
  {
    method: "POST",
    headers: { "Authorization": `Bearer ${apiKey}`, "Content-Type": "application/json" },
    body: JSON.stringify({ fileId, version, storageKey, sha256, size }),
  },
);

if (res.status === 409) {
  // Version was taken by a concurrent upload — re-initiate and retry.
}
const { confirmed } = await res.json();
POST/api/v1/projects/{projectId}/commits

Create commit

Commit confirmed file versions with a message. The array is `fileVersions`, and each entry carries the `version` you confirmed plus an action of add, modify, or delete. Commits are sequentially numbered per project; the response returns `sequenceNum` and HTTP 201. Max 25,000 files per request — for larger pushes use the draft-commit endpoints, which stage refs across many calls and finalize them as one atomic commit. Send `X-Idempotency-Key` to make a retry safe. A 409 with `code: "file_locked"` means a teammate holds a lock on one of the files.

curlbash
curl -X POST https://usourcecontrol.com/api/v1/projects/7c9e6679-7425-40de-944b-e07fc1f90ae7/commits \
  -H "Authorization: Bearer usc_your_project_key" \
  -H "Content-Type: application/json" \
  -H "X-Idempotency-Key: 8f14e45f-ceea-467a-9f6b-1d3c2b7a5e90" \
  -d '{
    "message": "Updated hero animations",
    "fileVersions": [
      { "fileId": "f47ac10b-58cc-4372-a567-0e02b2c3d479", "version": 12, "action": "modify" },
      { "fileId": "9c0f1a77-4e2b-4d18-b3aa-51c7e9d0b246", "version": 1, "action": "add" }
    ]
  }'
Request bodyjson
{
  "message": "Updated hero animations",
  "branch": "main",
  "fileVersions": [
    { "fileId": "f47ac10b-58cc-4372-a567-0e02b2c3d479", "version": 12, "action": "modify" },
    { "fileId": "9c0f1a77-4e2b-4d18-b3aa-51c7e9d0b246", "version": 1, "action": "add" }
  ]
}
Responsejson
{
  "id": "c1d2e3f4-5a6b-7c8d-9e0f-1a2b3c4d5e6f",
  "sequenceNum": 25,
  "message": "Updated hero animations",
  "fileCount": 2
}
TypeScriptts
const res = await fetch(`https://usourcecontrol.com/api/v1/projects/${projectId}/commits`, {
  method: "POST",
  headers: {
    "Authorization": `Bearer ${apiKey}`,
    "Content-Type": "application/json",
    "X-Idempotency-Key": crypto.randomUUID(),
  },
  body: JSON.stringify({
    message: "Updated hero animations",
    // action is "add" | "modify" | "delete" — version is required on add/modify.
    fileVersions: confirmed.map(f => ({
      fileId: f.fileId,
      version: f.version,
      action: f.action,
    })),
  }),
});

const commit = await res.json();
if (res.status === 409 && commit.code === "file_locked") {
  console.error(commit.message, commit.lockedFiles);
} else {
  console.log(`Commit #${commit.sequenceNum} created`);
}
GET/api/v1/projects/{projectId}/files/{fileId}/versions

List file versions

Committed main-lineage versions of a file, newest first, each with a short-lived presigned download URL. The response is keyset-paginated as `data` + `nextCursor` — pass the cursor back as `?cursor=` to page, and `?limit=` (max 200) to size the page. Versions that only exist on a branch are not returned here.

curlbash
curl -X GET "https://usourcecontrol.com/api/v1/projects/7c9e6679-7425-40de-944b-e07fc1f90ae7/files/f47ac10b-58cc-4372-a567-0e02b2c3d479/versions?limit=50" \
  -H "Authorization: Bearer usc_your_project_key"
Request body
No body. Key is sent via Authorization header.
Responsejson
{
  "data": [
    {
      "id": "5e1c8a90-2b7d-4f3e-8c11-9a0b4d6e2f38",
      "version": 12,
      "size": 524288000,
      "sha256": "a1b2c3...",
      "uploaderId": "3f1a0c52-9c1e-4f0a-9a1b-7d2e5c8b4a10",
      "uploaderName": "Sarah Chen",
      "commitId": "c1d2e3f4-5a6b-7c8d-9e0f-1a2b3c4d5e6f",
      "downloadUrl": "https://s3.eu-central-003.backblazeb2.com/...",
      "createdAt": "2026-04-16T14:30:00.000Z"
    }
  ],
  "nextCursor": "12"
}
TypeScriptts
const res = await fetch(
  `https://usourcecontrol.com/api/v1/projects/${projectId}/files/${fileId}/versions?limit=50`,
  { headers: { "Authorization": `Bearer ${apiKey}` } },
);
const { data, nextCursor } = await res.json();
const latest = data[0];
console.log(`v${latest.version} by ${latest.uploaderName}`);

// Download URLs are short-lived presigned links — fetch them promptly.
await fetch(latest.downloadUrl);

// Page with the cursor until it comes back null.
if (nextCursor) { /* ...?cursor=${nextCursor} */ }
POST/api/v1/projects/{projectId}/restore

Restore file version

Roll one or more files back to an earlier version. The target field is `toVersion`. Nothing is overwritten: each restore writes a NEW version pointing at the old content, and the whole batch lands as a single restore commit. Files whose target version doesn't exist (or whose blob is missing) come back in `skipped` rather than failing the call. Max 25,000 files per request.

curlbash
curl -X POST https://usourcecontrol.com/api/v1/projects/7c9e6679-7425-40de-944b-e07fc1f90ae7/restore \
  -H "Authorization: Bearer usc_your_project_key" \
  -H "Content-Type: application/json" \
  -d '{
    "files": [
      { "fileId": "f47ac10b-58cc-4372-a567-0e02b2c3d479", "toVersion": 8 }
    ]
  }'
Request bodyjson
{
  "files": [
    { "fileId": "f47ac10b-58cc-4372-a567-0e02b2c3d479", "toVersion": 8 }
  ]
}
Responsejson
{
  "commitId": "d4c3b2a1-6f5e-4d3c-8b2a-1f0e9d8c7b6a",
  "sequenceNum": 26,
  "restored": [
    { "fileId": "f47ac10b-58cc-4372-a567-0e02b2c3d479", "newVersion": 13 }
  ],
  "skipped": []
}
TypeScriptts
const res = await fetch(`https://usourcecontrol.com/api/v1/projects/${projectId}/restore`, {
  method: "POST",
  headers: { "Authorization": `Bearer ${apiKey}`, "Content-Type": "application/json" },
  body: JSON.stringify({
    files: [{ fileId, toVersion: 8 }], // note: toVersion, not version
  }),
});
const { commitId, sequenceNum, restored, skipped } = await res.json();
console.log(`Restore landed as commit #${sequenceNum}`);
console.log(`v8 is now v${restored[0].newVersion}`);
if (skipped.length) console.warn("Skipped:", skipped);
GET · POST · DELETE/api/v1/projects/{projectId}/locks

File locks (exclusive checkout)

Binary Unreal assets can't be merged, so a lock reserves one for editing. GET lists active locks. POST acquires them for a set of `fileIds` (with an optional `note` up to 280 chars) and returns what you got plus what someone else already holds. DELETE releases them — you can always release your own; org owners and admins may pass `force: true` to release a lock held by someone else. Committing a file automatically releases the lock you held on it, and the commit endpoint rejects a commit touching a file locked by anyone else.

curlbash
# List active locks
curl -X GET https://usourcecontrol.com/api/v1/projects/7c9e6679-7425-40de-944b-e07fc1f90ae7/locks \
  -H "Authorization: Bearer usc_your_project_key"

# Acquire
curl -X POST https://usourcecontrol.com/api/v1/projects/7c9e6679-7425-40de-944b-e07fc1f90ae7/locks \
  -H "Authorization: Bearer usc_your_project_key" \
  -H "Content-Type: application/json" \
  -d '{ "fileIds": ["f47ac10b-58cc-4372-a567-0e02b2c3d479"], "note": "polishing lighting on L3" }'

# Release (add "force": true as an owner/admin to break someone else's lock)
curl -X DELETE https://usourcecontrol.com/api/v1/projects/7c9e6679-7425-40de-944b-e07fc1f90ae7/locks \
  -H "Authorization: Bearer usc_your_project_key" \
  -H "Content-Type: application/json" \
  -d '{ "fileIds": ["f47ac10b-58cc-4372-a567-0e02b2c3d479"] }'
Request bodyjson
{
  "fileIds": ["f47ac10b-58cc-4372-a567-0e02b2c3d479"],
  "note": "polishing lighting on L3"
}
Responsejson
{
  "acquired": [
    {
      "fileId": "f47ac10b-58cc-4372-a567-0e02b2c3d479",
      "path": "Content/Maps/MainLevel.umap",
      "userId": "3f1a0c52-9c1e-4f0a-9a1b-7d2e5c8b4a10",
      "userName": "Sarah Chen",
      "userAvatarUrl": null,
      "isMine": true,
      "note": "polishing lighting on L3",
      "lockedAt": "2026-04-16T14:30:00.000Z"
    }
  ],
  "conflicts": []
}
TypeScriptts
// Acquire before editing a binary asset.
const res = await fetch(`https://usourcecontrol.com/api/v1/projects/${projectId}/locks`, {
  method: "POST",
  headers: { "Authorization": `Bearer ${apiKey}`, "Content-Type": "application/json" },
  body: JSON.stringify({ fileIds, note: "polishing lighting on L3" }),
});
const { acquired, conflicts } = await res.json();
if (conflicts.length) {
  // Someone else holds these — don't start editing.
  console.warn(conflicts.map(c => `${c.path} (${c.userName})`));
}

// Releasing is implicit on commit; this is only for abandoning an edit.
await fetch(`https://usourcecontrol.com/api/v1/projects/${projectId}/locks`, {
  method: "DELETE",
  headers: { "Authorization": `Bearer ${apiKey}`, "Content-Type": "application/json" },
  body: JSON.stringify({ fileIds }),
});