Runlog docs

Start building

A plan buys a weekly allowance, sized for steady work.

Start

Files

For bytes you hold — on disk, in your own bucket, or as a whole codebase. Two ways in: hand over signed URLs and the relay fetches them, or content-address locally and upload only the chunks the server is missing. Either way bytes never pass through the relay as request bodies, which is what makes a 2 GB file and a one-line edit to it cost about the same on the wire, and the file id stays stable across every later edit.

Bring

Getting bytes in, and watching them land. One call takes any number of files and answers the project's ingestion lease; a second call says where that lease stands. Both are keyed on the lease rather than on a file, so a corpus of ten thousand files is still one thing to send and one thing to watch.

POST/v1/projects/{projectId}/ingestAPI key

Ingest files

Read one file or a whole corpus into a project, in a single call, and get back the lease reading it.

One call, any number of files. The bytes ride the request, so there is no chunk handshake to run and no upload URL to sign — a whole corpus lands in as many calls as its size needs, not as many calls as it has files.

Every call for one project answers the SAME `leaseId`, because a project has exactly one ingestion lease: a second batch JOINS the reading already running rather than starting a rival one. Three uploads, one lease, one thing to watch.

The reading itself happens inside the platform, not on your key. The rate limit prices your CALLS, and this is one call — the model work behind it draws no request from your bucket.

A refusal is per file and never stops the rest of the call. Every refusal names its path and says why. A credential-shaped file is left on disk unless you name that exact path with `includeSecret`.

A path the project already holds REPLACES its own record — same file id, previous reading retired — so re-sending a changed file never leaves two copies of it in the project.

Body

FieldTypeRequiredDescription
filesobject[]requiredThe files to read in.
files[].pathstringrequiredPath relative to the project root.
files[].contentstringrequiredThe file's bytes, base64.
files[].contentTypestringoptionalDefaults from the path's extension.
files[].includeSecretbooleanoptionalRead this one path despite its shape.
intentstringoptionalWhat this batch is for, applied once it has landed.
Request · curl
curl -X POST "https://runlog-7613480744.us-central1.run.app/v1/projects/p7Kd2mQx/ingest" \
  -H "Authorization: Bearer $RUNLOG_API_KEY" \
  -H "X-Project-Id: p7Kd2mQx" \
  -H "Content-Type: application/json" \
  -d '{
  "files": [
    {
      "content": "IyBDaGVja291dAo=",
      "contentType": "text/markdown",
      "path": "specs/checkout.md"
    }
  ],
  "version": 1
}'
Response · 202
{
  "accepted": [
    {
      "fileId": "4mHqZ1nR8vTbC0sWyLpAeGkJ3dXfU6iO2rN",
      "path": "specs/checkout.md",
      "status": "queued"
    }
  ],
  "leaseId": "p7Kd2mQx",
  "projectId": "p7Kd2mQx",
  "refused": [
    {
      "path": ".env",
      "reason": "this one looks like it holds a secret, so it is left alone"
    }
  ],
  "version": 1
}

Errors

StatuserrorWhen
400this call carries no filesThe body named no files.
403editor access requiredYou can read the project but not write to it.
404project not foundThe project does not exist, or you have no access to it. Projects are non-enumerable, so a denied read is indistinguishable from a missing one.
413this request was too large to read; send fewer files at a timeOne call carried more than 24 MB. Split the corpus; the lease is the same either way.
401invalid API keyThe `Authorization` header is missing, malformed, or names a revoked key.
400name the target project via the X-Project-Id header or projectId queryNo project was named and the key is not bound to one.
429rate limit exceededYou passed 60 requests/minute on this key. `Retry-After` carries the seconds to wait.
500internal errorThe relay failed to serve the request. Retry with backoff; the cause is logged server-side against your request.
GET/v1/projects/{projectId}/ingestAPI key

Watch an ingestion

Ask where the project's ingestion lease stands: what it took, what is left, and what failed.

Keyed on the lease, not on a file. However many files you sent and in however many calls, this is the one thing to poll — which is what keeps watching an ingestion from costing a request per file.

`enqueued` is everything this lease has taken since it opened, `remaining` what it has not driven yet. A project with nothing in flight holds no lease and answers `active: false` with every count at zero, which is how a finished ingestion reads.

`failed` names the project's files whose reading errored, each carrying why in the words you can act on.

Request · curl
curl -X GET "https://runlog-7613480744.us-central1.run.app/v1/projects/p7Kd2mQx/ingest" \
  -H "Authorization: Bearer $RUNLOG_API_KEY" \
  -H "X-Project-Id: p7Kd2mQx"
Response · 200
{
  "active": true,
  "completed": 375,
  "enqueued": 412,
  "failed": [],
  "leaseId": "p7Kd2mQx",
  "pending": [
    {
      "fileId": "4mHqZ1nR8vTbC0sWyLpAeGkJ3dXfU6iO2rN",
      "path": "specs/checkout.md",
      "percent": 62,
      "phase": "Understanding entities (3/8)",
      "status": "processing"
    }
  ],
  "projectId": "p7Kd2mQx",
  "remaining": 37,
  "version": 1
}

Errors

StatuserrorWhen
404project not foundThe project does not exist, or you have no access to it. Projects are non-enumerable, so a denied read is indistinguishable from a missing one.
401invalid API keyThe `Authorization` header is missing, malformed, or names a revoked key.
400name the target project via the X-Project-Id header or projectId queryNo project was named and the key is not bound to one.
429rate limit exceededYou passed 60 requests/minute on this key. `Retry-After` carries the seconds to wait.
500internal errorThe relay failed to serve the request. Retry with backoff; the cause is logged server-side against your request.
POST/v1/api/actionsAPI key

Register one uploaded file

Register a single content-addressed file you already pushed to storage, in one dispatch.

The one-call sibling of the register route below: same content addressing, same queue, one dispatch. Reach for the REST register when you are driving the chunk-diff flow yourself.

Dispatched as `{"action": "file_upload", "input": {...}}`.

Body

FieldTypeRequiredDescription
actionstringrequiredMust be `"file_upload"`.
input.fileIdstringrequiredContent-derived id of the whole file (43 base62 chars).
input.namestringrequiredThe file's display name.
input.extstringoptionalLowercased extension or mime subtype (e.g. txt, md, pdf).
input.fileTypestringoptionalMime content type stamped on the chunk blob.
input.fileSizenumberoptionalTotal byte size of the file.
input.filepathstringoptionalFolder the file lands in within the project — a client value; the server resolves the session project and overrides the destination, so a forged path lands in the session root.
input.chunkIdsstring[]requiredOrdered chunk ids whose bytes reassemble the file.
input.chunkingstringoptionalChunking mode: 'sections' | 'bytes' | '' for a single chunk.
Request · curl
curl -X POST "https://runlog-7613480744.us-central1.run.app/v1/api/actions" \
  -H "Authorization: Bearer $RUNLOG_API_KEY" \
  -H "X-Project-Id: p7Kd2mQx" \
  -H "Content-Type: application/json" \
  -d '{
  "action": "file_upload",
  "input": {
    "chunkIds": [
      "4mHqZ1nR8vTbC0sWyLpAeGkJ3dXfU6iO2rN"
    ],
    "ext": "swift",
    "fileId": "4mHqZ1nR8vTbC0sWyLpAeGkJ3dXfU6iO2rN",
    "name": "Router.swift"
  }
}'
Response · 200
{
  "action": "file_upload",
  "result": {
    "fileId": "4mHqZ1nR8vTbC0sWyLpAeGkJ3dXfU6iO2rN",
    "status": "queued"
  }
}

Errors

StatuserrorWhen
400fileId, name and chunkIds are requiredA required field was absent.
403action not available to an API keyThe key's tier does not reach this action.
400invalid request bodyThe body is not JSON, or exceeds the 1 MB request-body cap.
401invalid API keyThe `Authorization` header is missing, malformed, or names a revoked key.
400name the target project via the X-Project-Id header or projectId queryNo project was named and the key is not bound to one.
429rate limit exceededYou passed 60 requests/minute on this key. `Retry-After` carries the seconds to wait.
500internal errorThe relay failed to serve the request. Retry with backoff; the cause is logged server-side against your request.
POST/v1/projects/{projectId}/files/importAPI key

Import from signed URLs

Hand the relay a list of signed URLs; it fetches each one and ingests it into the named project.

For files already in your own bucket. You never move the bytes: sign a read URL per object with whatever expiry you are comfortable with, post the list, and the relay pulls each one directly from your storage.

The call returns as soon as the batch is accepted, one file record per source, each starting at `queued`. Ingestion is asynchronous — watch it settle on the file stream rather than blocking on this response.

A URL that has expired, 404s, or exceeds the per-file ceiling fails that source alone and is reported by name in `failed`. The rest of the batch still lands, so a bad URL in a list of two hundred does not cost you the batch.

Content is addressed after the fetch, so importing an object you have already ingested stores no new bytes and re-registering the same content is idempotent.

Path parameters

FieldTypeRequiredDescription
projectIdstringrequiredThe project the files land in.

Body

FieldTypeRequiredDescription
sourcesobject[]requiredThe objects to fetch, up to 500 per call.
sources[].urlstringrequiredA signed, time-limited read URL. GCS, S3, and any HTTPS URL that serves the bytes without further auth.
sources[].namestringoptionalFile name to store it under. Defaults to the URL's last path segment with its query stripped.
sources[].filepathstringoptionalFolder to place it in, as a `/`-joined relative path. Absent means the project root.
sources[].titlestringoptionalDisplay title, if it should differ from the file name.
Request · curl
curl -X POST "https://runlog-7613480744.us-central1.run.app/v1/projects/p7Kd2mQx/files/import" \
  -H "Authorization: Bearer $RUNLOG_API_KEY" \
  -H "X-Project-Id: p7Kd2mQx" \
  -H "Content-Type: application/json" \
  -d '{
  "sources": [
    {
      "filepath": "drawings",
      "url": "https://storage.googleapis.com/acme-drawings/site-plan.pdf?X-Goog-Signature=…"
    },
    {
      "filepath": "drawings",
      "name": "north-elevation.png",
      "url": "https://storage.googleapis.com/acme-drawings/elevation.png?X-Goog-Signature=…"
    }
  ]
}'
Response · 202
{
  "accepted": 2,
  "failed": [],
  "files": [
    {
      "fileId": "4mHqZ1nR8vTbC0sWyLpAeGkJ3dXfU6iO2rN",
      "filepath": "drawings",
      "name": "site-plan.pdf",
      "status": "queued"
    },
    {
      "fileId": "Yt4RcVbNm2QaZsXdCfVgBhNjMkLoPiUyTrE",
      "filepath": "drawings",
      "name": "north-elevation.png",
      "status": "queued"
    }
  ]
}

Errors

StatuserrorWhen
400sources is requiredThe body named no sources, or more than 500.
403editor access requiredYou can read the project but not write to it.
404project not foundThe project does not exist, or you have no access to it. Projects are non-enumerable, so a denied read is indistinguishable from a missing one.
413file too largeA fetched object exceeds the per-file ceiling. Reported per source; the batch still lands.
400invalid request bodyThe body is not JSON, or exceeds the 1 MB request-body cap.
401invalid API keyThe `Authorization` header is missing, malformed, or names a revoked key.
400name the target project via the X-Project-Id header or projectId queryNo project was named and the key is not bound to one.
429rate limit exceededYou passed 60 requests/minute on this key. `Retry-After` carries the seconds to wait.
500internal errorThe relay failed to serve the request. Retry with backoff; the cause is logged server-side against your request.
POST/v1/projects/{projectId}/filesAPI key

Register an uploaded file

Record the file against its ordered chunk ids and queue its first ingestion pass.

This is step 4 of the large-file diff flow, and the only step that writes a record. Call it after every missing chunk body has landed in storage; registering against a chunk that is not stored leaves a file whose ingestion cannot drain.

`filepath` is the folder, not the file name — the two are separate fields. Directories are never records: the project tree is read back off these strings, so a folder exists exactly as long as a file claims it.

The response returns immediately with `status: "queued"`. Ingestion is asynchronous; poll the file record or subscribe to the file stream to watch it settle.

Keep the `fileId` this returns. It is what you tether a later retrieval to, and once the file reaches `ready` it is also how you read back the entity names its ingestion derived.

Path parameters

FieldTypeRequiredDescription
projectIdstringrequiredThe project the file lands in.

Body

FieldTypeRequiredDescription
fileIdstringrequiredThe derived file id. For a single-chunk file this IS the chunk id; for a multi-chunk file it is the base62 SHA-256 of the ordered chunk ids joined by newlines.
namestringrequiredFile name including its extension, e.g. `Router.swift`.
chunkIdsstring[]requiredThe ordered chunk ids whose bodies reassemble the file.
extstringoptionalLowercase extension without the dot. Must match the `ext` sent to `chunks/missing`.
fileTypestringoptionalCoarse class, e.g. `code`, `document`, `table`.
fileSizeintegeroptionalTotal bytes, for display and for the per-file size ceiling.
filepathstringoptionalThe file's folder relative to the project root. `""` is the root. A folder upload sends each file's own directory here.
chunkingstringoptional`""` for a single-chunk file, `"sections"` when each chunk is independently a valid file of the same extension, `"bytes"` for a raw byte split that only reassembles in order.
intentstringoptionalDeclared purpose, e.g. `"public API surface"`, applied after the first pass.
replacesPlannedstring[]optionalFile ids this registration supersedes. The server retires each named representation once this one is registered, instead of accumulating duplicates. Accepted and ignored today — retire the old file with an explicit DELETE until this ships. See the replacement indicator below.
Request · curl
curl -X POST "https://runlog-7613480744.us-central1.run.app/v1/projects/p7Kd2mQx/files" \
  -H "Authorization: Bearer $RUNLOG_API_KEY" \
  -H "X-Project-Id: p7Kd2mQx" \
  -H "Content-Type: application/json" \
  -d '{
  "chunkIds": [
    "4mHqZ1nR8vTbC0sWyLpAeGkJ3dXfU6iO2rN"
  ],
  "chunking": "",
  "ext": "swift",
  "fileId": "4mHqZ1nR8vTbC0sWyLpAeGkJ3dXfU6iO2rN",
  "fileSize": 41822,
  "fileType": "code",
  "filepath": "Sources/Net",
  "name": "Router.swift"
}'
Response · 200
{
  "chunkIds": [
    "4mHqZ1nR8vTbC0sWyLpAeGkJ3dXfU6iO2rN"
  ],
  "chunking": "",
  "createdAt": "2026-08-07T18:22:10Z",
  "ext": "swift",
  "fileId": "4mHqZ1nR8vTbC0sWyLpAeGkJ3dXfU6iO2rN",
  "fileSize": 41822,
  "fileType": "code",
  "filepath": "Sources/Net",
  "name": "Router.swift",
  "projectId": "p7Kd2mQx",
  "status": "queued",
  "updatedAt": "2026-08-07T18:22:10Z"
}

Errors

StatuserrorWhen
400fileId, name and chunkIds are requiredOne of the three required fields was absent or empty.
403editor access requiredYou can read the project but not write to it.
404project not foundThe project does not exist, or you have no access to it. Projects are non-enumerable, so a denied read is indistinguishable from a missing one.
500failed to enqueue ingestionThe record was written but the ingestion queue rejected it. Re-register the same body; registration is idempotent on `fileId`.
400invalid request bodyThe body is not JSON, or exceeds the 1 MB request-body cap.
401invalid API keyThe `Authorization` header is missing, malformed, or names a revoked key.
400name the target project via the X-Project-Id header or projectId queryNo project was named and the key is not bound to one.
429rate limit exceededYou passed 60 requests/minute on this key. `Retry-After` carries the seconds to wait.
500internal errorThe relay failed to serve the request. Retry with backoff; the cause is logged server-side against your request.
POST/v1/projects/{projectId}/files/chunks/missingAPI key

Ask which chunks are missing

Send the chunk manifest; get back only the chunk ids the server does not already hold, each with a signed upload URL.

This is step 2 of the large-file diff flow. Send the ids of EVERY chunk in the file, not just the ones you believe changed — the server owns the answer about what it already has, and sending the whole manifest is what lets an unchanged chunk cost zero bytes.

Chunk blobs are global and content-addressed. A chunk already stored by a different file, a different project, or a different account still counts as present, so a vendored dependency shared across repositories uploads exactly once across the whole corpus.

Each returned URL is a net-new signed PUT valid for 30 minutes. Upload the chunk body with a plain `PUT` to that URL — no `Authorization` header, the signature is the credential.

Path parameters

FieldTypeRequiredDescription
projectIdstringrequiredThe project the file lands in.

Body

FieldTypeRequiredDescription
chunkIdsstring[]requiredEvery chunk id in the file, in order. A chunk id is the base62 SHA-256 of the chunk bytes, truncated to 43 characters.
extstringoptionalLowercase file extension without the dot. It decides the storage path (`chunks/{chunkId}.{ext}`) and must match the `ext` you register.
Request · curl
curl -X POST "https://runlog-7613480744.us-central1.run.app/v1/projects/p7Kd2mQx/files/chunks/missing" \
  -H "Authorization: Bearer $RUNLOG_API_KEY" \
  -H "X-Project-Id: p7Kd2mQx" \
  -H "Content-Type: application/json" \
  -d '{
  "chunkIds": [
    "4mHqZ1nR8vTbC0sWyLpAeGkJ3dXfU6iO2rN",
    "9wEr5TyUiOpAsDfGhJkLzXcVbNm1QaZ2sXd",
    "Kp3LmNq7RtVwXyZ0aBcDeFgHiJkLmNoPqRs"
  ],
  "ext": "swift"
}'
Response · 200
{
  "missing": [
    "9wEr5TyUiOpAsDfGhJkLzXcVbNm1QaZ2sXd"
  ],
  "uploadUrls": {
    "9wEr5TyUiOpAsDfGhJkLzXcVbNm1QaZ2sXd": "https://storage.googleapis.com/runlog-chunks/chunks/9wEr5TyUiOpAsDfGhJkLzXcVbNm1QaZ2sXd.swift?X-Goog-Signature=..."
  }
}

Errors

StatuserrorWhen
400chunkIds are requiredThe manifest was empty.
403editor access requiredYou can read the project but not write to it.
404project not foundThe project does not exist, or you have no access to it. Projects are non-enumerable, so a denied read is indistinguishable from a missing one.
500failed to sign upload URLStorage would not mint a signed URL. Retry with backoff.
400invalid request bodyThe body is not JSON, or exceeds the 1 MB request-body cap.
401invalid API keyThe `Authorization` header is missing, malformed, or names a revoked key.
400name the target project via the X-Project-Id header or projectId queryNo project was named and the key is not bound to one.
429rate limit exceededYou passed 60 requests/minute on this key. `Retry-After` carries the seconds to wait.
500internal errorThe relay failed to serve the request. Retry with backoff; the cause is logged server-side against your request.
POST/v1/projects/{projectId}/files/zipAPI key

Upload a codebase as a zip

Register an uploaded archive; the server expands it, preserving each entry's relative path.

The archive itself uploads through the same chunk flow — content-address the `.zip`, PUT its missing chunks, then register it here instead of through the file route.

This is the server-side counterpart of the folder walk. Expansion applies the same repository-noise filter a client-side walk applies, and every dropped entry comes back named in `ignoredPaths` rather than vanishing.

The route answers 200 once every entry is staged and enqueued, even if the source archive could not be cleaned up afterwards — that lands in `undeletedZipFileId` and `cleanupError`. A non-2xx would invite a retry that re-ingests the whole archive.

Path parameters

FieldTypeRequiredDescription
projectIdstringrequiredThe project the archive expands into.

Body

FieldTypeRequiredDescription
fileIdstringrequiredThe already-uploaded archive's content-addressed id.
filepathstringoptionalFolder the archive's entries expand under. `""` is the project root.
fileSizeintegeroptionalArchive size in bytes.
Request · curl
curl -X POST "https://runlog-7613480744.us-central1.run.app/v1/projects/p7Kd2mQx/files/zip" \
  -H "Authorization: Bearer $RUNLOG_API_KEY" \
  -H "X-Project-Id: p7Kd2mQx" \
  -H "Content-Type: application/json" \
  -d '{
  "fileId": "Zq8Xr2Nv5TbW9cYmLpKeAgHiJ3dXfU6iO2r",
  "fileSize": 18412663,
  "filepath": "checkout"
}'
Response · 200
{
  "cleanupError": "",
  "createdFiles": [
    {
      "fileId": "4mHqZ1nR8vTbC0sWyLpAeGkJ3dXfU6iO2rN",
      "filepath": "checkout/Sources/Net",
      "name": "Router.swift"
    },
    {
      "fileId": "Bn6VcXz9QwErTyUiOpAsDfGhJkLmNoPqRsT",
      "filepath": "checkout",
      "name": "Package.swift"
    }
  ],
  "ignoredPaths": [
    "checkout/.git/HEAD",
    "checkout/node_modules/lodash/index.js",
    "checkout/.build/debug/Router.o"
  ],
  "undeletedZipFileId": ""
}

Errors

StatuserrorWhen
400fileId is requiredThe body named no archive.
400invalid or corrupt zip fileThe stored bytes are not a readable zip archive.
413zip file exceeds the size limitThe archive as a whole is over the per-file ceiling.
413zip entry exceeds the per-file limitOne entry inside the archive is over the per-file ceiling. Split the upload.
403editor access requiredYou can read the project but not write to it.
404project not foundThe project does not exist, or you have no access to it. Projects are non-enumerable, so a denied read is indistinguishable from a missing one.
401invalid API keyThe `Authorization` header is missing, malformed, or names a revoked key.
400name the target project via the X-Project-Id header or projectId queryNo project was named and the key is not bound to one.
429rate limit exceededYou passed 60 requests/minute on this key. `Retry-After` carries the seconds to wait.
500internal errorThe relay failed to serve the request. Retry with backoff; the cause is logged server-side against your request.

Browse

Reading what the project holds: the file records, the folder tree derived from them, and the entity names an ingestion resolved — the ids every later retrieval tethers to.

GET/v1/projects/{projectId}/filesAPI key

List files

One cursor page of the project's visible files, newest cursor on `X-Next-Cursor`.

Soft-deleted files never appear. The body is a bare array; page it to exhaustion by following `X-Next-Cursor` until the header stops coming back.

Path parameters

FieldTypeRequiredDescription
projectIdstringrequiredThe project to list.

Query parameters

FieldTypeRequiredDescription
cursorstringoptionalCursor from a previous page.
limitintegeroptionalPage size, capped at 200.
Request · curl
curl -X GET "https://runlog-7613480744.us-central1.run.app/v1/projects/p7Kd2mQx/files?limit=100" \
  -H "Authorization: Bearer $RUNLOG_API_KEY" \
  -H "X-Project-Id: p7Kd2mQx"
Response · 200
[
  {
    "chunkIds": [
      "4mHqZ1nR8vTbC0sWyLpAeGkJ3dXfU6iO2rN"
    ],
    "chunking": "",
    "createdAt": "2026-08-07T18:22:10Z",
    "ext": "swift",
    "fileId": "4mHqZ1nR8vTbC0sWyLpAeGkJ3dXfU6iO2rN",
    "fileSize": 41822,
    "fileType": "code",
    "filepath": "Sources/Net",
    "name": "Router.swift",
    "projectId": "p7Kd2mQx",
    "status": "queued",
    "updatedAt": "2026-08-07T18:22:10Z"
  }
]

Errors

StatuserrorWhen
404project not foundThe project does not exist, or you have no access to it. Projects are non-enumerable, so a denied read is indistinguishable from a missing one.
401invalid API keyThe `Authorization` header is missing, malformed, or names a revoked key.
400name the target project via the X-Project-Id header or projectId queryNo project was named and the key is not bound to one.
429rate limit exceededYou passed 60 requests/minute on this key. `Retry-After` carries the seconds to wait.
500internal errorThe relay failed to serve the request. Retry with backoff; the cause is logged server-side against your request.
GET/v1/projects/{projectId}/files/{fileId}API key

Get a file

Read one file record: where its reading got to, and everything the project knows about it.

`status` moves `queued` → `processing` → `ready`, or lands on `error` with the cause on the record. A soft-deleted or unknown file answers 404.

For ONE file you are waiting on. To watch an ingestion, read the lease instead — one call however many files it holds, rather than one call per file.

Path parameters

FieldTypeRequiredDescription
projectIdstringrequiredThe owning project.
fileIdstringrequiredThe file to read.
Request · curl
curl -X GET "https://runlog-7613480744.us-central1.run.app/v1/projects/p7Kd2mQx/files/4mHqZ1nR8vTbC0sWyLpAeGkJ3dXfU6iO2rN" \
  -H "Authorization: Bearer $RUNLOG_API_KEY" \
  -H "X-Project-Id: p7Kd2mQx"
Response · 200
{
  "chunkIds": [
    "4mHqZ1nR8vTbC0sWyLpAeGkJ3dXfU6iO2rN"
  ],
  "chunking": "",
  "createdAt": "2026-08-07T18:22:10Z",
  "ext": "swift",
  "fileId": "4mHqZ1nR8vTbC0sWyLpAeGkJ3dXfU6iO2rN",
  "fileSize": 41822,
  "fileType": "code",
  "filepath": "Sources/Net",
  "name": "Router.swift",
  "projectId": "p7Kd2mQx",
  "status": "ready",
  "updatedAt": "2026-08-07T18:22:10Z"
}

Errors

StatuserrorWhen
404file not foundThe file does not exist in this project, or was soft-deleted.
404project not foundThe project does not exist, or you have no access to it. Projects are non-enumerable, so a denied read is indistinguishable from a missing one.
401invalid API keyThe `Authorization` header is missing, malformed, or names a revoked key.
400name the target project via the X-Project-Id header or projectId queryNo project was named and the key is not bound to one.
429rate limit exceededYou passed 60 requests/minute on this key. `Retry-After` carries the seconds to wait.
500internal errorThe relay failed to serve the request. Retry with backoff; the cause is logged server-side against your request.
GET/v1/projects/{projectId}/foldersAPI key

List folders

Read the project's folder tree, derived from the `filepath` every file carries.

Path parameters

FieldTypeRequiredDescription
projectIdstringrequiredThe project to read.
Request · curl
curl -X GET "https://runlog-7613480744.us-central1.run.app/v1/projects/p7Kd2mQx/folders" \
  -H "Authorization: Bearer $RUNLOG_API_KEY" \
  -H "X-Project-Id: p7Kd2mQx"
Response · 200
[
  {
    "fileCount": 2,
    "path": "checkout"
  },
  {
    "fileCount": 14,
    "path": "checkout/Sources/Net"
  }
]

Errors

StatuserrorWhen
404project not foundThe project does not exist, or you have no access to it. Projects are non-enumerable, so a denied read is indistinguishable from a missing one.
401invalid API keyThe `Authorization` header is missing, malformed, or names a revoked key.
400name the target project via the X-Project-Id header or projectId queryNo project was named and the key is not bound to one.
429rate limit exceededYou passed 60 requests/minute on this key. `Retry-After` carries the seconds to wait.
500internal errorThe relay failed to serve the request. Retry with backoff; the cause is logged server-side against your request.
GET/v1/projects/{projectId}/files/{fileId}/entitiesAPI key

Read what a file resolved to

The names of every entity the file's ingestion derived — the other half of what an ingestion gives you back.

Registering a file returns its `fileId` immediately. The entities do not exist until the derivation pass has run, so they are read here rather than promised on a response written before they were derived.

These names are exactly the strings the retrieval lane accepts as an `entities` tether, and the `fileId` is what it accepts as a `fileIds` tether. Ingest, read this, then ask your question anchored to both.

A file still ingesting answers `200` with an empty list. Nothing is wrong — the names are not derived yet, and `status` on the file record is the field that says which it is.

Path parameters

FieldTypeRequiredDescription
projectIdstringrequiredThe owning project.
fileIdstringrequiredThe ingested file.
Request · curl
curl -X GET "https://runlog-7613480744.us-central1.run.app/v1/projects/p7Kd2mQx/files/4mHqZ1nR8vTbC0sWyLpAeGkJ3dXfU6iO2rN/entities" \
  -H "Authorization: Bearer $RUNLOG_API_KEY" \
  -H "X-Project-Id: p7Kd2mQx"
Response · 200
{
  "entityCount": 3,
  "entityNames": [
    "Curtain Wall Type A",
    "North Elevation",
    "Level 3 Slab"
  ],
  "fileId": "4mHqZ1nR8vTbC0sWyLpAeGkJ3dXfU6iO2rN",
  "status": "ready"
}

Errors

StatuserrorWhen
404file not foundThe file does not exist in this project, or was soft-deleted.
404project not foundThe project does not exist, or you have no access to it. Projects are non-enumerable, so a denied read is indistinguishable from a missing one.
401invalid API keyThe `Authorization` header is missing, malformed, or names a revoked key.
400name the target project via the X-Project-Id header or projectId queryNo project was named and the key is not bound to one.
429rate limit exceededYou passed 60 requests/minute on this key. `Retry-After` carries the seconds to wait.
500internal errorThe relay failed to serve the request. Retry with backoff; the cause is logged server-side against your request.

Edit

Placement changes. A move rewrites the path and nothing else; content changes live in the Migration leg, because a new version of a file is a migration.

PATCH/v1/projects/{projectId}/files/{fileId}API key

Rename or move a file

Change a file's name, title, or folder. A `filepath` change is a move.

A move is a pure placement rewrite: the file id, its content, and its ingestion state are all unchanged.

At least one of the three fields is required; a field you do not name is left alone.

Path parameters

FieldTypeRequiredDescription
projectIdstringrequiredThe owning project.
fileIdstringrequiredThe file to change.

Body

FieldTypeRequiredDescription
namestringoptionalNew file name including extension.
titlestringoptionalNew display title.
filepathstringoptionalNew folder. `""` moves the file to the project root.
Request · curl
curl -X PATCH "https://runlog-7613480744.us-central1.run.app/v1/projects/p7Kd2mQx/files/4mHqZ1nR8vTbC0sWyLpAeGkJ3dXfU6iO2rN" \
  -H "Authorization: Bearer $RUNLOG_API_KEY" \
  -H "X-Project-Id: p7Kd2mQx" \
  -H "Content-Type: application/json" \
  -d '{
  "filepath": "Sources/Networking"
}'
Response · 200
{
  "chunkIds": [
    "4mHqZ1nR8vTbC0sWyLpAeGkJ3dXfU6iO2rN"
  ],
  "chunking": "",
  "createdAt": "2026-08-07T18:22:10Z",
  "ext": "swift",
  "fileId": "4mHqZ1nR8vTbC0sWyLpAeGkJ3dXfU6iO2rN",
  "fileSize": 41822,
  "fileType": "code",
  "filepath": "Sources/Networking",
  "name": "Router.swift",
  "projectId": "p7Kd2mQx",
  "status": "queued",
  "updatedAt": "2026-08-07T18:22:10Z"
}

Errors

StatuserrorWhen
400at least one field is requiredThe body named none of `name`, `title`, `filepath`.
404file not foundNo such file in this project.
403editor access requiredYou can read the project but not write to it.
404project not foundThe project does not exist, or you have no access to it. Projects are non-enumerable, so a denied read is indistinguishable from a missing one.
401invalid API keyThe `Authorization` header is missing, malformed, or names a revoked key.
400name the target project via the X-Project-Id header or projectId queryNo project was named and the key is not bound to one.
429rate limit exceededYou passed 60 requests/minute on this key. `Retry-After` carries the seconds to wait.
500internal errorThe relay failed to serve the request. Retry with backoff; the cause is logged server-side against your request.
PATCH/v1/projects/{projectId}/folders/{folderPath}API key

Move a folder

Relocate a whole subtree in one batched prefix rewrite.

One batch rewrites every contained file's `filepath`. File ids never change, and the move either lands wholly or fails — never a silent half-move.

Moving a folder across the `.runlog/Artifacts/` boundary is the promote/demote gesture; see below.

Path parameters

FieldTypeRequiredDescription
projectIdstringrequiredThe owning project.
folderPathstringrequiredThe folder to move, as a slash-separated path.

Body

FieldTypeRequiredDescription
newPathstringrequiredDestination path for the subtree.
Request · curl
curl -X PATCH "https://runlog-7613480744.us-central1.run.app/v1/projects/p7Kd2mQx/folders/src/net" \
  -H "Authorization: Bearer $RUNLOG_API_KEY" \
  -H "X-Project-Id: p7Kd2mQx" \
  -H "Content-Type: application/json" \
  -d '{
  "newPath": "Sources/Transport"
}'
Response · 200
{
  "from": "Sources/Net",
  "moved": 14,
  "to": "Sources/Transport"
}

Errors

StatuserrorWhen
400invalid folder moveThe move is structurally impossible — the project root, a no-op, or a folder into its own subtree.
403editor access requiredYou can read the project but not write to it.
404project not foundThe project does not exist, or you have no access to it. Projects are non-enumerable, so a denied read is indistinguishable from a missing one.
500internal errorThe batch failed partway. The move is reported failed rather than partially applied.
401invalid API keyThe `Authorization` header is missing, malformed, or names a revoked key.
400name the target project via the X-Project-Id header or projectId queryNo project was named and the key is not bound to one.
429rate limit exceededYou passed 60 requests/minute on this key. `Retry-After` carries the seconds to wait.
500internal errorThe relay failed to serve the request. Retry with backoff; the cause is logged server-side against your request.
DELETE/v1/projects/{projectId}/files/{fileId}API key

Delete a file

Soft-delete a file and retire the derivations that rested only on it.

Chunk blobs are global and shared, so a delete never removes bytes another file still references. Re-registering the same content later costs no upload.

Path parameters

FieldTypeRequiredDescription
projectIdstringrequiredThe owning project.
fileIdstringrequiredThe file to delete.
Request · curl
curl -X DELETE "https://runlog-7613480744.us-central1.run.app/v1/projects/p7Kd2mQx/files/4mHqZ1nR8vTbC0sWyLpAeGkJ3dXfU6iO2rN" \
  -H "Authorization: Bearer $RUNLOG_API_KEY" \
  -H "X-Project-Id: p7Kd2mQx"
Response · 200
{
  "deleted": true,
  "fileId": "4mHqZ1nR8vTbC0sWyLpAeGkJ3dXfU6iO2rN"
}

Errors

StatuserrorWhen
404file not foundNo such file in this project.
403editor access requiredYou can read the project but not write to it.
404project not foundThe project does not exist, or you have no access to it. Projects are non-enumerable, so a denied read is indistinguishable from a missing one.
401invalid API keyThe `Authorization` header is missing, malformed, or names a revoked key.
400name the target project via the X-Project-Id header or projectId queryNo project was named and the key is not bound to one.
429rate limit exceededYou passed 60 requests/minute on this key. `Retry-After` carries the seconds to wait.
500internal errorThe relay failed to serve the request. Retry with backoff; the cause is logged server-side against your request.