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

# Files

> Upload your organization's documents into a private knowledge base and search them alongside the web: presign, upload, index, then search with the workspace scope.

The Files endpoints give every organization a private knowledge base. Upload documents once and they become searchable through the same `POST /v1/search` call you already use for the web — scoped to your organization, invisible to everyone else.

The flow is always the same four steps:

1. **Presign** — `POST /v1/files/presign` returns a short-lived upload URL bound to your file's exact size.
2. **Upload** — `PUT` the raw bytes to that URL. The bytes go straight to storage; they never pass through the API.
3. **Index** — `POST /v1/files/index` starts an indexing run; poll `GET /v1/files/index/{sync_id}` until it completes.
4. **Search** — query with `scope.indexes: ["workspace"]` and your organization id as `workspace_id`.

The SDKs and CLI collapse steps 1–3 into a single call.

<CodeGroup>
  ```bash CLI theme={"system"}
  caesar-search files upload ./q3-report.pdf
  # uploaded q3-report.pdf (1.8 MB)
  # indexing started: 7f3c2a1e-... (queued)

  caesar-search files status 7f3c2a1e-...
  # 7f3c2a1e-...: completed
  ```

  ```python Python theme={"system"}
  from caesar_search import Caesar

  client = Caesar()
  upload = client.upload_file("q3-report.pdf")           # presign + upload + index
  status = client.file_index_status(upload.sync_id)      # poll until "completed"

  hits = client.search(
      "q3 revenue",
      extra_body={"scope": {"indexes": ["workspace"], "workspace_id": "<your-org-id>"}},
  )
  ```

  ```typescript TypeScript theme={"system"}
  import { readFileSync } from "node:fs"; // works in Node, Bun, and Deno
  import { Caesar } from "caesar-search";

  const caesar = new Caesar();
  const upload = await caesar.uploadFile({
    filename: "q3-report.pdf",
    data: readFileSync("./q3-report.pdf"),
    contentType: "application/pdf",
  });
  const status = await caesar.fileIndexStatus(upload.sync_id!); // poll until "completed"

  const hits = await caesar.search("q3 revenue", {
    extraBody: { scope: { indexes: ["workspace"], workspace_id: "<your-org-id>" } },
  });
  ```

  ```bash cURL theme={"system"}
  # 1. Presign (size must be the exact byte count)
  curl -s https://alpha.api.trycaesar.com/v1/files/presign \
    -H "Authorization: Bearer $CAESAR_API_KEY" \
    -H "Content-Type: application/json" \
    -d '{"filename": "q3-report.pdf", "size": 1895311, "content_type": "application/pdf"}'

  # 2. Upload — note: NO Authorization header; the URL's signature authorizes it
  curl -s -X PUT "$PRESIGNED_URL" \
    -H "Content-Type: application/pdf" \
    --data-binary @q3-report.pdf

  # 3. Index, then poll
  curl -s -X POST https://alpha.api.trycaesar.com/v1/files/index \
    -H "Authorization: Bearer $CAESAR_API_KEY" -H "Content-Type: application/json" -d '{}'
  curl -s https://alpha.api.trycaesar.com/v1/files/index/$SYNC_ID \
    -H "Authorization: Bearer $CAESAR_API_KEY"
  ```
</CodeGroup>

## Searching your files

Workspace search runs through the ordinary search endpoint with a scope:

```json theme={"system"}
{
  "query": "q3 revenue",
  "scope": { "indexes": ["workspace"], "workspace_id": "<your-org-id>" }
}
```

* `workspace_id` is your organization id; the API verifies it matches the key's organization, so no other tenant's files are ever reachable.
* Use `"indexes": ["web", "workspace"]` to blend web and file results in one response; workspace hits are tagged `"index": "workspace"` and ranked separately from web results.
* Workspace results carry the same provenance handles as web results — `doc_id`, passages, and a `source_uri` pointing at the stored object — and can be opened with `POST /v1/document` by `doc_id`.

## Managing files

| Operation           | Endpoint                                                                                            |
| ------------------- | --------------------------------------------------------------------------------------------------- |
| List uploads        | `GET /v1/files` → `{ files: [{ name, size, last_modified }] }`                                      |
| Delete by name      | `DELETE /v1/files/{name}` — the document drops out of search after the automatic reconciliation run |
| Re-index everything | `POST /v1/files/index` with `{ "mode": "full" }`                                                    |

Filenames are the identity: the API deals in the sanitized `name` echoed by presign, never in storage paths. Uploading the same filename again replaces the file (and re-indexing updates the document in place).

## Indexing runs

`POST /v1/files/index` accepts `{ "mode": "incremental" }` (default — new and changed files only) or `{ "mode": "full" }` (reprocess everything). It answers `202` with a `sync_id`; poll `GET /v1/files/index/{sync_id}`:

```json theme={"system"}
{
  "sync_id": "7f3c2a1e-...",
  "state": "completed",
  "stats": { "enumerated": 3, "fetched": 1, "indexed": 3, "failed": 0, "skipped_unsupported": 0, "deleted": 0, "bytes": 1895311 },
  "error": null
}
```

Terminal states are `completed` and `failed`. The SDK/CLI upload helpers trigger one incremental run per upload call by default; when uploading many files, batch them (`index=False` / `--no-index`) and start one run at the end.

## Supported types and limits

| Constraint             | Value                                                                                                                    |
| ---------------------- | ------------------------------------------------------------------------------------------------------------------------ |
| File types             | `pdf`, `html`, `htm`, `txt`, `md`, `docx`, `pptx`, `xlsx`, `doc`, `ppt`, `xls`, `rtf`, `odt`, `odp`, `ods`, `csv`, `tsv` |
| Max file size          | 100 MB per file (`max_object_bytes` in the presign response is authoritative)                                            |
| Presigned URL lifetime | 15 minutes (`expires_in_seconds`)                                                                                        |

Unsupported types are rejected at presign time with `415 unsupported_file_type`; oversized files with `413 file_too_large`.

## How presigned uploads work

The presign response's `url` is a signed storage URL:

* It authorizes exactly one `PUT` of exactly the declared `size`, for 15 minutes. A body of any other length is rejected by storage with `403`.
* Send **no `Authorization` header** on the `PUT` — the signature in the URL is the authorization, and your API key must never be sent to storage.
* Treat the URL like a short-lived token: anyone holding it can upload that one object until it expires.
