Skip to main content
npm install caesar-search
The quickstart runs without a key — the anonymous tier works at a lower rate limit:
import { Caesar } from "caesar-search";

const caesar = new Caesar(); // reads CAESAR_API_KEY; anonymous works at a lower rate limit

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 doc = await caesar.read(results.results?.[0]?.doc_id, { maxChars: 8000 });
console.log(doc.content?.text);

// Close the loop: feedback improves ranking
await caesar.feedback("result_helpful", {
  searchId: results.search_id,
  docId: results.results?.[0]?.doc_id,
  rank: 1,
});
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 simply behave as anonymous/default.

Configuration

const caesar = new Caesar({ timeoutMs: 10000, maxRetries: 1 });
OptionEnvironment variableDefault
apiKeyCAESAR_API_KEYanonymous (lower rate limit)
baseUrlCAESAR_BASE_URLhttps://search-api-staging-779189860552.europe-west1.run.app
maxRetries3 — retries 429/5xx, honors Retry-After; 0 disables
timeoutMs30000 — per-request timeout in milliseconds

Methods

async search(query: string, options?: SearchOptions): Promise<SearchResponse>
async read(target?: string, options?: ReadOptions): Promise<DocumentResponse>
async feedback(eventType: string, options?: FeedbackOptions): Promise<FeedbackResponse>
interface SearchOptions {
  mode?: "fast" | "standard" | "research";
  maxResults?: number;
  objective?: string;
  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"]; passing a query selects query-relevant content, otherwise the full document, always as markdown. See documents for the response shape.

Continuation reads

When doc.content?.truncated is true, resume from where the last read ended:
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

await caesar.search("query", { verbosity: "compact", maxCharsTotal: 4000 });
verbosity and maxCharsTotal map onto the wire response block — see response shaping.

Errors and retries

All error classes are importable from the package root:
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);
  }
}
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 (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. A Retry-After header is honored when it is a numeric number of seconds (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 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 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.