> ## 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.

# TypeScript SDK

> Install and use the caesar-search package for Node, Bun, Deno, and edge runtimes.

```bash theme={"system"}
npm install caesar-search
```

Set `CAESAR_API_KEY` before running the quickstart:

```ts theme={"system"}
import { Caesar } from "caesar-search";

const caesar = new Caesar(); // reads CAESAR_API_KEY

const results = await caesar.search("postgres 17 logical replication failover", {
  maxResults: 5,
});

for (const result of results.results ?? []) {
  console.log(result.rank, result.title, result.canonical_url);
}

// Read a result as clean markdown — pass a doc_id or a URL
const first = results.results?.[0];
if (!first) throw new Error("No results");

const doc = await caesar.read(first.doc_id, { maxChars: 8000 });
console.log(doc.content?.text ?? "");
```

The package ships an ESM + CJS dual build with full type definitions. It requires Node `>=20` and works in Bun, Deno, and edge runtimes — environment variables are read through a guard, so runtimes without `process` should pass `apiKey` explicitly.

## Configuration

```ts theme={"system"}
const caesar = new Caesar({ timeoutMs: 10000, maxRetries: 1 });
```

| Option       | Environment variable | Default                                           |
| ------------ | -------------------- | ------------------------------------------------- |
| `apiKey`     | `CAESAR_API_KEY`     | required                                          |
| `baseUrl`    | `CAESAR_BASE_URL`    | `https://alpha.api.trycaesar.com`                 |
| `maxRetries` | —                    | `3` — retries 429/5xx; `0` disables               |
| `timeoutMs`  | —                    | `30000` — per-request timeout in **milliseconds** |

## Methods

```ts theme={"system"}
async search(query: string, options?: SearchOptions): Promise<SearchResponse>
async read(target?: string, options?: ReadOptions): Promise<DocumentResponse>
async feedback(eventType: string, options?: FeedbackOptions): Promise<FeedbackResponse>

// Files knowledge base
async uploadFile(options: UploadFileOptions): Promise<UploadFileResult>
async presignUpload(filename: string, size: number, options?: PresignUploadOptions): Promise<FilePresignResponse>
async listFiles(): Promise<FileListResponse>
async deleteFile(name: string): Promise<FileDeleteResponse>
async indexFiles(options?: IndexFilesOptions): Promise<FileIndexResponse>
async fileIndexStatus(syncId: string): Promise<FileIndexStatusResponse>
```

```ts theme={"system"}
interface SearchOptions {
  maxResults?: number;
  sessionId?: string;
  verbosity?: "ids_only" | "compact" | "standard" | "full";
  maxCharsTotal?: number;   // becomes response.budget.max_chars_total
  extraBody?: Record<string, unknown>;
}

interface ReadOptions {
  docId?: string;
  url?: string;
  query?: string;
  maxChars?: number;
  startChar?: number;       // Continue a truncated read from this character offset.
  include?: string[];
  extraBody?: Record<string, unknown>;
}

interface FeedbackOptions {
  searchId?: string;
  docId?: string;
  passageId?: string;
  query?: string;
  rank?: number;
  notes?: string;
  extraBody?: Record<string, unknown>;
}
```

### How read() resolves its target

The first positional `target` is auto-detected: a UUID-shaped string is sent as `doc_id`, anything else as `canonical_url`. Explicit `docId`/`url` options win if both are given. If neither resolves, the SDK throws `TypeError("provide a docId or a url")`. Default `include` is `["metadata", "content"]`; content selection is `full_document`; content format is markdown; `maxChars` is omitted unless you pass it. See [documents](/concepts/documents) for the response shape.

### Continuation reads

When `doc.content?.truncated` is true, resume from where the last read ended:

```ts theme={"system"}
const next = await caesar.read(docId, {
  startChar: (doc.content?.start_char ?? 0) + (doc.content?.char_count ?? 0),
});
```

A non-zero `startChar` forces full-document selection and addresses raw document text, so offsets stay contiguous between calls — it will not combine with query-relevant selection.

### Response shaping

```ts theme={"system"}
await caesar.search("query", { verbosity: "compact", maxCharsTotal: 4000 });
```

`verbosity` and `maxCharsTotal` map onto the wire `response` block — see [response shaping](/concepts/response-shaping).

### Uploading files

`uploadFile()` collapses the presign → upload → index flow into one call. `data` accepts a `Blob`, `ArrayBuffer`, `Uint8Array`, or string (UTF-8 encoded):

```ts theme={"system"}
import { readFileSync } from "node:fs"; // works in Node, Bun, and Deno

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>" } },
});
```

The file bytes go straight to storage via the presigned URL — the API key is never sent there. Batch several uploads with `index: false`, then call `indexFiles()` once. See [Files](/concepts/files) for supported types, limits, and the workspace search scope.

## Errors and retries

All error classes are importable from the package root:

```ts theme={"system"}
import { Caesar, APIStatusError, RateLimitError } from "caesar-search";

try {
  await caesar.search("postgres 17");
} catch (err) {
  if (err instanceof APIStatusError) {
    console.error(err.statusCode, err.code, err.requestId);
  }
}
```

```text theme={"system"}
CaesarError                  base class
├── APIConnectionError       the API could not be reached
│   └── APITimeoutError      request timed out
└── APIStatusError           non-2xx response — .statusCode, .code, .requestId, .response
    ├── AuthenticationError  HTTP 401 or 403
    └── RateLimitError       HTTP 429
```

`.code` comes from the API [error envelope](/concepts/errors) (`error.code`), falling back to `http_<status>`; `.requestId` carries the server's `request_id`.

Retry semantics: statuses 429, 500, 502, 503, and 504 are retried up to `maxRetries` times (default 3 retries, so up to 4 attempts) with exponential backoff of 0.5s doubling per attempt, capped at 8 seconds. Caesar rate limits use `X-RateLimit-Reset`; if a numeric `Retry-After` header is present, the client honors it, also capped at 8s. Timeouts and connection failures are not retried — they throw `APITimeoutError` / `APIConnectionError` immediately. Each attempt gets its own `timeoutMs` abort signal.

## Raw responses and extra fields

`caesar.withResponse.search(...)` (also `.read`, `.feedback`) takes the same arguments and returns `{ data, response }`, where `response` is the fetch `Response` — useful for reading [rate-limit headers](/rate-limits) or status. For request fields the options don't model yet, pass `extraBody`; it is merged last and can override any field.

All generated request and response types (`SearchResponse`, `DocumentResponse`, `FeedbackResponse`, `SearchRequest`, `FeedbackRequest`, and more) are re-exported from the package root.

## For agents

* `timeoutMs` is **milliseconds** (`30000` = 30s). The [Python SDK](/clients/sdks/python) uses `timeout` in seconds — do not transplant values between them.
* Response collections and nested objects are typed as optional. Guard with `??` and `?.` exactly as the quickstart does (`results.results ?? []`, `doc.content?.text`).
* Request options are camelCase (`maxResults`, `sessionId`, `docId`); response fields are snake\_case as the API returns them (`search_id`, `doc_id`, `canonical_url`, `start_char`, `char_count`). Mixing the two directions is the most common bug.
