> ## Documentation Index
> Fetch the complete documentation index at: https://hubify.com/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Files API

> Read, write, and delete lab files. Backs the local-agents and CLI workflows.

# Files

Read and write any file in a lab, notes, papers, scripts, datasets. Authenticated callers see the same tree the orchestrator and Files panel see. When the lab has a linked GitHub repo with auto-push enabled, write/delete operations also commit to the repo.

**Base URL:** `https://www.hubify.com/api/v1`

**Required scope:**

* `lab:<slug>:r` for `GET`
* `lab:<slug>:rw` for `POST` / `PATCH` / `DELETE`

The wildcard `lab:*:r` / `lab:*:rw` / `lab:*:admin` also works.

## List files in a lab

<CodeGroup>
  ```bash curl theme={null}
  curl "https://www.hubify.com/api/v1/files?labId=<id>" \
    -H "Authorization: Bearer $HUBIFY_TOKEN"
  ```

  ```typescript TypeScript theme={null}
  const res = await fetch(
    `https://www.hubify.com/api/v1/files?labId=${labId}`,
    { headers: { Authorization: `Bearer ${process.env.HUBIFY_TOKEN}` } }
  );
  const { files } = await res.json();
  ```
</CodeGroup>

Returns the full file tree:

```json theme={null}
{
  "files": [
    {
      "_id": "k57...",
      "labId": "k88...",
      "name": "today.md",
      "path": "/notes/today.md",
      "type": "file",
      "parentPath": "/notes",
      "mimeType": "text/markdown",
      "size": 4123,
      "content": "...",
      "createdAt": 1714368000000,
      "updatedAt": 1714368042000
    }
  ],
  "total": 1
}
```

## Read a single file

<CodeGroup>
  ```bash curl theme={null}
  curl "https://www.hubify.com/api/v1/files?labId=<id>&path=/notes/today.md" \
    -H "Authorization: Bearer $HUBIFY_TOKEN"
  ```

  ```typescript TypeScript theme={null}
  const res = await fetch(
    `https://www.hubify.com/api/v1/files?labId=${labId}&path=/notes/today.md`,
    { headers: { Authorization: `Bearer ${process.env.HUBIFY_TOKEN}` } }
  );
  const { files } = await res.json();
  const file = files[0]; // single file at the given path
  ```
</CodeGroup>

Returns just that file's record. 404 if the path does not exist.

## Create or overwrite a file

<CodeGroup>
  ```bash curl theme={null}
  curl -X POST https://www.hubify.com/api/v1/files \
    -H "Authorization: Bearer $HUBIFY_TOKEN" \
    -H "Content-Type: application/json" \
    -d '{
      "labId": "<id>",
      "path": "/notes/today.md",
      "content": "# Today\n\nMeeting notes...",
      "mimeType": "text/markdown"
    }'
  ```

  ```typescript TypeScript theme={null}
  const res = await fetch("https://www.hubify.com/api/v1/files", {
    method: "POST",
    headers: {
      Authorization: `Bearer ${process.env.HUBIFY_TOKEN}`,
      "Content-Type": "application/json",
    },
    body: JSON.stringify({
      labId,
      path: "/notes/today.md",
      content: "# Today\n\nMeeting notes...",
      mimeType: "text/markdown",
    }),
  });
  const { created, fileId, bytes } = await res.json();
  ```
</CodeGroup>

If a file already exists at `path`, this updates its content. Otherwise it creates the file (and the implied parent folder structure). MIME type is inferred from the extension if omitted.

Response:

```json theme={null}
{ "created": true, "path": "/notes/today.md", "fileId": "k7...", "bytes": 28 }
```

## Update content only

<CodeGroup>
  ```bash curl theme={null}
  curl -X PATCH https://www.hubify.com/api/v1/files \
    -H "Authorization: Bearer $HUBIFY_TOKEN" \
    -H "Content-Type: application/json" \
    -d '{ "labId": "<id>", "path": "/notes/today.md", "content": "..." }'
  ```

  ```typescript TypeScript theme={null}
  const res = await fetch("https://www.hubify.com/api/v1/files", {
    method: "PATCH",
    headers: {
      Authorization: `Bearer ${process.env.HUBIFY_TOKEN}`,
      "Content-Type": "application/json",
    },
    body: JSON.stringify({ labId, path: "/notes/today.md", content: "..." }),
  });
  ```
</CodeGroup>

Same shape as `POST`, but returns `404` if no file exists at `path`. Use this when you specifically don't want to create new files by accident.

## Delete a file

<CodeGroup>
  ```bash curl theme={null}
  curl -X DELETE \
    "https://www.hubify.com/api/v1/files?labId=<id>&path=/notes/today.md" \
    -H "Authorization: Bearer $HUBIFY_TOKEN"
  ```

  ```typescript TypeScript theme={null}
  const res = await fetch(
    `https://www.hubify.com/api/v1/files?labId=${labId}&path=/notes/today.md`,
    {
      method: "DELETE",
      headers: { Authorization: `Bearer ${process.env.HUBIFY_TOKEN}` },
    }
  );
  const { deleted } = await res.json();
  ```
</CodeGroup>

Response:

```json theme={null}
{ "deleted": true, "path": "/notes/today.md" }
```

## GitHub round-trip

When a lab has a GitHub repo linked and `gitAutoPushOnEdit` is true (toggle in Lab Settings):

* `POST` / `PATCH` writes also `PUT /repos/.../contents/{path}` on GitHub
* `DELETE` also `DELETE /repos/.../contents/{path}` on GitHub

The Hubify request returns success as long as the Convex write succeeded, GitHub push errors are logged on the lab's activity feed but don't fail the API call. Use [`GET /api/v1/sync`](/docs/api/sync) to confirm the latest sync SHA.

See [Local Agents guide](/docs/guides/local-agents) for the full bidirectional flow.

## Errors

| Status | Meaning                                                   |
| ------ | --------------------------------------------------------- |
| 400    | Missing `labId` / `path` / `content`                      |
| 401    | Invalid or missing API key                                |
| 403    | Key lacks `lab:<slug>:r` (GET) or `lab:<slug>:rw` (write) |
| 404    | No file at the given path (GET / PATCH / DELETE)          |
| 429    | Rate limit (120/min read, 30/min write)                   |
| 500    | Convex error, check response body                         |
