# Agents guide
Source: https://docs.trycaesar.com/agents/agents-guide
Which Caesar surface to use from which agent harness, and the conventions that hold across all of them.
Caesar is one API with several client surfaces. Pick the surface that matches your harness; everything else — verbs, field names, presets, limits — is identical across them.
For agents reading the docs: use `/llms.txt` to discover pages, append `.md` to page URLs for markdown, and start with [CLI vs MCP](/agents/cli-vs-mcp) when choosing an agent surface.
## Pick your surface
| Your harness | Use | Why |
| ------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------- |
| Inner-loop shell work (Claude Code, terminal agents, scripts) | The [CLI](/clients/cli/usage), plus the [forked skills](/clients/skills) in Claude Code | `--json` machine output, `-o ` keeps large payloads out of your context window, stable exit codes to branch on |
| Chat or IDE assistant (Cursor, Windsurf, any MCP client) | The [remote MCP server](/clients/mcp/remote) at `/mcp?profile=web-search` | Zero install, streamable HTTP, generic `web_search` / `web_fetch` names so ordinary web searches route to Caesar |
| Building an application | [Python SDK](/clients/sdks/python) or [TypeScript SDK](/clients/sdks/typescript); [`caesar-search/ai`](/clients/sdks/ai-sdk) for Vercel AI SDK agents | Typed clients with retries built in; `caesarTools()` is a drop-in tool set |
| Everything else | Raw HTTP — spec served at `/openapi/public.json` on the API host | See the [API reference](/api-reference/index) |
## Same three verbs everywhere
| Verb | REST | CLI | MCP | SDKs |
| -------- | ------------------- | ------------------------ | ------------ | ------------------- |
| Search | `POST /v1/search` | `caesar-search search` | `web_search` | `client.search()` |
| Read | `POST /v1/document` | `caesar-search read` | `web_fetch` | `client.read()` |
| Feedback | `POST /v1/feedback` | `caesar-search feedback` | not exposed | `client.feedback()` |
The same search on each surface:
```bash cURL theme={"system"}
curl -s https://alpha.api.trycaesar.com/v1/search \
-H 'Authorization: Bearer $CAESAR_API_KEY' \
-H 'Content-Type: application/json' \
-d '{"query":"zig comptime explained","max_results":5,"response":{"verbosity":"compact"}}'
```
```python Python theme={"system"}
from caesar_search import Caesar
client = Caesar() # reads CAESAR_API_KEY
results = client.search("zig comptime explained", max_results=5, verbosity="compact")
```
```ts TypeScript theme={"system"}
import { Caesar } from "caesar-search";
const caesar = new Caesar(); // reads CAESAR_API_KEY
const results = await caesar.search("zig comptime explained", {
maxResults: 5,
verbosity: "compact",
});
```
```bash CLI theme={"system"}
caesar-search search "zig comptime explained" --max-results 5 --format compact --json
```
## Conventions that hold on every surface
### Wire fields are snake\_case
`doc_id`, `search_id`, `max_results`, `start_char`, `canonical_url` — exactly as the API returns them. No surface converts to camelCase. Do not expect `docId` in any response.
### One set of verbosity presets
`verbosity` (REST/SDKs), `--format` (CLI `search`), and `response_format` (MCP, AI SDK tools) are the same presets. Use `compact` in agent loops — it is the token-efficient choice. `standard` adds quotable passages; `full` adds [provenance](/concepts/provenance). See [response shaping](/concepts/response-shaping).
| Surface | Parameter | Values | Default |
| ------------ | ---------------------------------- | ------------------------------------------------------------------ | ---------- |
| REST / SDKs | `response.verbosity` / `verbosity` | `ids_only`, `compact`, `standard`, `full` | `standard` |
| CLI | `--format` | `ids_only`, `compact`, `standard`, `full` | `standard` |
| MCP | `response_format` | `compact`, `standard`, `full` (`ids_only` falls back to `compact`) | `compact` |
| AI SDK tools | `response_format` | `compact`, `standard`, `full` | `compact` |
### max\_results is 1-50
REST and SDKs default to 10 and reject out-of-range values with `400 validation_error`. The CLI also defaults to 10 but rejects out-of-range values locally before any request is sent (`bad_input`, exit 2). MCP defaults to 8 and silently clamps out-of-range values into 1-50. The AI SDK tools (`caesarTools()`) also default to 8, like MCP.
### Continue truncated reads with start\_char
A truncated read reports `truncated: true` with `start_char` and `char_count` (nested under `content` on REST/CLI/SDKs, top-level over MCP). Continue from `start_char + char_count`:
```bash theme={"system"}
caesar-search read "$DOC_ID" --start-char 12000 --json
```
Do not retry with a bigger `max_chars`. A non-zero `start_char` forces `full_document` selection so offsets stay contiguous — a `query` passed alongside it will not excerpt.
### Preserve identifiers, cite only returned URLs
`doc_id` and `search_id` are opaque IDs: thread them verbatim between search, read, and feedback. Cite only URLs the API returned (`canonical_url`, `url`, `source_url`) — never construct or guess a URL.
### Errors and retries
All API errors use one envelope:
```json theme={"system"}
{
"type": "error",
"request_id": "ee314ccd-dbcd-418f-9c6a-f2917c599c67",
"error": { "code": "rate_limited", "message": "rate limit exceeded" }
}
```
Retry 429 and 5xx with exponential backoff; the CLI and SDKs already do (3 retries, 500 ms doubling, 8 s cap). Rate-limit windows are one second — the `X-RateLimit-Reset` header says when the window resets. Do not retry other 4xx. The CLI mirrors errors as a JSON envelope on stderr and exits 2 (bad input), 3 (auth), 4 (API error), or 5 (timeout) — branch on exit codes, not output. See [errors](/concepts/errors) and [rate limits](/rate-limits).
### API keys are required
Set `CAESAR_API_KEY` (CI), run `caesar-search auth login` (interactive — opens a browser and stores a named, revocable key; `--device` over SSH), or connect the Caesar MCP server in an OAuth-capable host, before search, read, or feedback calls. A missing credential returns `401 missing_api_key`; a present-but-invalid key returns `401 invalid_api_key`. See [authentication](/authentication).
### Optional feedback
When your app can tell which result helped, send feedback with the `search_id` and `doc_id` you got back. Use `result_helpful` when a result answered the task and `stale_result` when content was outdated:
```bash theme={"system"}
caesar-search feedback --event-type result_helpful --search-id "$SID" --doc-id "$DID"
```
Feedback is deliberately not exposed over MCP; use any other surface.
## Install
Decide which surface to give an agent first, and when to use both.
Copy-paste install blocks per environment, each with a verification step.
# CLI vs MCP for agents
Source: https://docs.trycaesar.com/agents/cli-vs-mcp
Choose Caesar's CLI, remote MCP server, or both for an agent workflow.
Agents can use Caesar through the [CLI](/clients/cli/install), the [remote MCP server](/clients/mcp/remote), or both. If the agent has shell access, start with the CLI. Use MCP when the client is built around remote tools or cannot run local commands.
Caesar's MCP server is intentionally small: two tools, `web_search` and `web_fetch`. That keeps its schema cost bounded. The broader MCP vs CLI tradeoff still matters when an agent session loads many MCP servers: tool schemas compete with task context, while CLI access is more progressive — the agent sees a short instruction, runs a command, and only then receives output.
## Quick answer
| Choose | When | Why |
| ------ | --------------------------------------------------------------- | ----------------------------------------------------------------------------------------------- |
| CLI | The agent has shell access, can write files, or runs in CI | Lowest context overhead, explicit exit codes, `--json`, and `-o` file capture for large outputs |
| MCP | The agent runs in a chat or IDE client with remote tool support | No local binary, discoverable tools, structured calls, and centralized auth |
| Both | The agent can use tools and shell commands | MCP gives easy interactive access; CLI handles scripts, artifacts, and repeatable automation |
For Claude Code and other coding agents with a shell, start with the CLI or the Caesar skill. For Cursor, Windsurf, and chat clients that already manage MCP servers, start with MCP.
## What changes for the agent
The agent runs `caesar-search`, reads stdout/stderr, checks exit codes, and can save JSON to files with `-o`.
The client connects to Caesar's remote server and exposes `web_search` and `web_fetch` as structured tools.
## Use the CLI when
* The agent can run shell commands.
* You want a lean prompt: a short skill or instruction can teach the command without loading tool schemas into every turn.
* You want deterministic scripting behavior: exit codes, stderr error envelopes, and `--json` output.
* Large search results should be written to a file instead of pasted into the chat context.
* The workflow is part of CI, a local script, or a repeatable terminal task.
* You want the same commands a human can run and debug.
* You want to restrict the agent's tool surface by documenting only the commands it should use.
Start here:
Install `caesar-search` with Homebrew, npm, or the standalone installer.
Search, read, feedback, JSON output, and exit codes.
Use `caesar-search` in scripts, schedulers, and CI pipelines.
## Use MCP when
* The agent client already supports remote MCP tools.
* You want a no-binary setup for a chat or IDE assistant.
* The client should discover the available Caesar tools instead of being prompted with shell commands.
* Auth should live in the MCP client configuration.
* The workflow is interactive and only needs search and read.
* The agent does not have a shell, or shell access is intentionally disabled.
MCP exposes exactly two tools: `web_search` and `web_fetch`. Feedback is REST, CLI, and SDK only.
Connect any MCP client to Caesar's hosted streamable-HTTP server.
## Use both when
Many agent setups benefit from both surfaces:
* MCP for quick interactive search and read inside the client.
* CLI for scripted runs, saved JSON artifacts, and commands the agent can replay outside the chat.
* Skills for coding agents that need durable instructions about when to choose each path.
The Caesar skill uses the CLI when available because shell agents handle command output and files well. MCP remains the better first choice for clients whose native integration model is remote tools.
## Keep the tool surface lean
For shell-capable agents, a small instruction file plus the CLI is often enough:
```text theme={"system"}
Use caesar-search for web search and document reads.
Prefer --json for machine output.
Use -o for large responses.
Never pass literal API keys; use CAESAR_API_KEY.
```
That is the Caesar version of progressive disclosure: document the command and constraints, then let the agent call the tool when the task needs it. Avoid giving an agent every possible surface at once. If the agent only needs search and read, install the Caesar skill or CLI; add MCP only when the client benefits from remote tool discovery.
## Decision checklist
| Question | Lean CLI | Lean MCP |
| ------------------------------------------------------------------------- | -------- | -------- |
| Does the agent have shell access? | yes | no |
| Does the workflow need `-o` files or CI artifacts? | yes | no |
| Does the client already manage MCP servers? | optional | yes |
| Does the workflow need only search and read? | either | yes |
| Does it need CLI-style automation, exit codes, or raw API escape hatches? | yes | no |
| Is the session already loaded with many MCP servers? | yes | no |
| Do you need dynamic tool discovery? | no | yes |
## External context
The broader MCP vs CLI discussion usually lands on a practical split: CLI is strongest for lean local or scripted work, MCP is strongest for structured remote tool access in clients that support it, and mature agent setups often use both. Recent critiques of MCP make the context-cost point more forcefully; for Caesar, that means defaulting shell agents to the CLI while keeping the small remote MCP server available for MCP-native clients.
* [CircleCI: MCP vs. CLI for AI-native development](https://circleci.com/blog/mcp-vs-cli/)
* [IBM Community: MCP vs CLI](https://community.ibm.com/community/user/blogs/jia-qi/2026/04/08/mcp-vs-cli)
# Install for agents
Source: https://docs.trycaesar.com/agents/install
Copy-paste Caesar install blocks for every agent environment, each with a verification step.
Every runtime block below expects `CAESAR_API_KEY`. [Sign up in the Caesar app](https://app.trycaesar.com), create an API key, and keep it in your environment, the CLI key store, or your MCP client config. For a limited time, all accounts receive a \$1,000 starting credit grant. Not sure which surface you need? Start with [CLI vs MCP](/agents/cli-vs-mcp).
In a hurry? For agents that support skills, run:
```bash theme={"system"}
npx skills add caesar-data/skills --skill caesar-search
```
That installs the Caesar search skill from the repo. The API-served installer below installs the full skill bundle plus the Claude Code subagent where supported.
## Setup states
| Path | Use when | Install signal | Verify signal |
| ------ | --------------------------------- | -------------------------------- | ------------------------------------------------------------- |
| Skill | Your agent reads `SKILL.md` | `caesar-search` skill is present | CLI search returns `search_id` |
| MCP | Your client supports remote tools | `caesar` server is listed | Web-search profile tools include `web_search` and `web_fetch` |
| CLI | Your agent has shell access | `caesar-search` is on `PATH` | `caesar-search search ... --json` exits 0 |
| AI SDK | You are building app tools | `caesar-search/ai` resolves | `caesarTools` imports as a function |
| REST | You want raw HTTP | no install required | first response includes `search_id` |
Paste one of these prompts into your agent and let it do the work:
```text Prompt: skill theme={"system"}
Set up Caesar search here:
1. Run: npx skills add caesar-data/skills --skill caesar-search
2. If this environment cannot run the Skills CLI, run:
curl -fsSL https://alpha.api.trycaesar.com/skills/install.sh | bash
3. If caesar-search is missing from PATH, install it with
"npm install -g caesar-search-cli" or "brew install caesar-data/tap/caesar-search".
4. Verify: caesar-search search "hello world" --max-results 1 --json
Requires CAESAR_API_KEY or a key stored with caesar-search auth login
(opens a browser; use --device over SSH).
5. Report the install path and the verification output.
```
```text Prompt: full bundle theme={"system"}
Set up Caesar search here:
1. Run: curl -fsSL https://alpha.api.trycaesar.com/skills/install.sh | bash
2. If caesar-search is missing from PATH, install it with
"npm install -g caesar-search-cli" or "brew install caesar-data/tap/caesar-search".
3. Verify: caesar-search search "hello world" --max-results 1 --json
Requires CAESAR_API_KEY or a key stored with caesar-search auth login
(opens a browser; use --device over SSH).
4. Report the install path and the verification output.
```
```text Prompt: MCP server theme={"system"}
Add the Caesar MCP server to this environment. The endpoint is
https://alpha.api.trycaesar.com/mcp?profile=web-search
(streamable HTTP). Send CAESAR_API_KEY as the header
"Authorization: Bearer ".
Verify by listing tools — you should see web_search and web_fetch. Use those
generic tools for ordinary web searches and URL reads.
```
## Claude Code
Fastest path for the search skill:
```bash theme={"system"}
npx skills add caesar-data/skills --skill caesar-search
```
Install the four skills (`caesar-search`, `caesar-read`, `caesar-setup`, `caesar-best-practices`) and the `caesar-subagent`:
```bash theme={"system"}
curl -fsSL https://alpha.api.trycaesar.com/skills/install.sh | bash
```
Restart the session (or run `/caesar-setup`) to pick them up. Verify:
```bash theme={"system"}
ls ~/.claude/skills | grep caesar
```
Expect four `caesar-*` directories. The skills drive the `caesar-search` CLI; if it is missing from PATH, `/caesar-setup` installs it.
Alternative (or addition): connect the remote MCP server instead.
```bash theme={"system"}
claude mcp add --transport http caesar "https://alpha.api.trycaesar.com/mcp?profile=web-search" \
--header "Authorization: Bearer $CAESAR_API_KEY"
```
Verify: `claude mcp list` shows `caesar` connected. To see its tools (`web_search` and `web_fetch` on the web-search profile), run `/mcp` inside a session.
## Cursor, Windsurf, and other MCP clients
Add the streamable-HTTP server to your client's MCP config (`.cursor/mcp.json`, Windsurf's `mcp_config.json`, or equivalent):
```json theme={"system"}
{
"mcpServers": {
"caesar": {
"url": "https://alpha.api.trycaesar.com/mcp?profile=web-search",
"headers": {
"Authorization": "Bearer $CAESAR_API_KEY"
}
}
}
}
```
Most clients do not expand environment variables inside config files — inject the key value when generating the config, and never commit a literal key.
Verify from any shell (the endpoint is stateless and answers plain JSON):
```bash theme={"system"}
curl -s "https://alpha.api.trycaesar.com/mcp?profile=web-search" \
-H "Authorization: Bearer $CAESAR_API_KEY" \
-H 'Content-Type: application/json' \
-H 'Accept: application/json, text/event-stream' \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}'
```
Expect exactly two tools: `web_search` and `web_fetch`. Details: [remote MCP server](/clients/mcp/remote).
## Any agent with a skills directory
If your agent uses the Skills CLI, install the search skill directly:
```bash theme={"system"}
npx skills add caesar-data/skills --skill caesar-search
```
The installer targets any skills directory via `CAESAR_SKILLS_DIR`:
```bash theme={"system"}
curl -fsSL https://alpha.api.trycaesar.com/skills/install.sh | \
CAESAR_SKILLS_DIR=~/.agents/skills bash
```
Verify: `ls ~/.agents/skills` shows the four `caesar-*` directories. With `CAESAR_SKILLS_DIR` set, the `caesar-subagent` file is skipped — subagent definitions are Claude Code-specific. Other harnesses ignore the `context: fork` frontmatter and run the skills inline; they still work. Details: [skills](/clients/skills).
If your agent uses the `skills` npm CLI, install from the GitHub shorthand:
```bash theme={"system"}
npx skills add caesar-data/skills --skill '*' -g -y
```
Install only the runtime search skill with:
```bash theme={"system"}
npx skills add caesar-data/skills --skill caesar-search -g -y
```
Do not use bare `npx skills add caesar-search`; `skills` expects a source such as `owner/repo`, a GitHub URL, or a local path.
For Claude Code, prefer the curl installer above because it also writes `~/.claude/agents/caesar-subagent.md`. The `npx skills` path installs the `SKILL.md` folders only.
## Plain shell agents (CLI)
Install `caesar-search` through one channel:
```bash Homebrew theme={"system"}
brew install caesar-data/tap/caesar-search
```
```bash npm theme={"system"}
npm install -g caesar-search-cli
```
```bash cURL theme={"system"}
curl -fsSL https://raw.githubusercontent.com/caesar-data/caesar-search-cli/main/install.sh | bash
```
npm needs Node 20+; the curl installer verifies checksums and lands in `~/.local/bin` (override with `CAESAR_INSTALL_DIR`). Verify:
```bash theme={"system"}
caesar-search search "test" --max-results 1 --json
```
Exit code 0 and a JSON object containing `search_id` and one result. Auth: `export CAESAR_API_KEY=...` or `caesar-search auth login` (browser login; `--device` for SSH). Usage and scripting contract: [CLI usage](/clients/cli/usage), [automation](/clients/cli/automation).
## Vercel AI SDK
```bash theme={"system"}
npm install caesar-search ai
```
The `ai` package (version 5 or later) is an optional peer dependency — install it explicitly. Then:
```ts theme={"system"}
import { generateText } from "ai";
import { caesarTools } from "caesar-search/ai";
const { text } = await generateText({
model,
tools: caesarTools(),
prompt: "What changed in Postgres 17 logical replication?",
});
```
`caesarTools()` returns `web_search` and `web_fetch` wired to a default client that reads `CAESAR_API_KEY`. Pass `caesarTools({ client })` to supply a configured `Caesar` instance. Verify the subpath resolves:
```bash theme={"system"}
node -e 'import("caesar-search/ai").then(m => console.log(typeof m.caesarTools))'
```
Prints `function`. Details: [AI SDK tools](/clients/sdks/ai-sdk).
## Raw HTTP
Nothing to install. One authenticated call:
```bash theme={"system"}
curl -s https://alpha.api.trycaesar.com/v1/search \
-H 'Authorization: Bearer $CAESAR_API_KEY' \
-H 'Content-Type: application/json' \
-d '{"query":"test","max_results":1,"response":{"verbosity":"compact"}}'
```
A 200 with a `search_id` confirms access. The OpenAPI spec is served unauthenticated at `/openapi/public.json` on the same host. See the [API reference](/api-reference/index).
## Troubleshooting
Auth failure. Set `CAESAR_API_KEY` or run `caesar-search auth login`. If a key is already set, it is malformed, expired, or revoked. Check state with `caesar-search auth status --json`.
The installed CLI is outdated. Run `caesar-search update` — it detects the install channel (npm, Homebrew, standalone) and upgrades in place — then retry the exact same command. Do not run `npm update -g` or `brew upgrade` by hand.
Over the per-key second limit. Back off and retry — the `X-RateLimit-Reset` header gives the window reset, at most about a second away. The CLI and SDKs retry 429 automatically with exponential backoff; if you still see 429, lower request volume or request a higher per-key limit. See [rate limits](/rate-limits).
Check the `Authorization` header: it must be `Bearer` followed by an active key. Auth failures on `/mcp` are HTTP-level, so the client reports a connection error rather than a tool error.
# Get a document
Source: https://docs.trycaesar.com/api-reference/documents/get-a-document
/api-reference/openapi.json post /v1/document
Inspect one canonical document and retrieve selected content.
# Record feedback
Source: https://docs.trycaesar.com/api-reference/feedback/record-feedback
/api-reference/openapi.json post /v1/feedback
Persist an agent or evaluation feedback event.
# Create a presigned upload URL
Source: https://docs.trycaesar.com/api-reference/files/create-a-presigned-upload-url
/api-reference/openapi.json post /v1/files/presign
Create a presigned S3 PUT URL for one file. Upload the raw bytes to the returned url with an HTTP PUT and no Authorization header; the body must be exactly the declared size. Then trigger POST /v1/files/index to make the file searchable via /v1/search with scope.indexes ["workspace"].
# Delete an uploaded file
Source: https://docs.trycaesar.com/api-reference/files/delete-an-uploaded-file
/api-reference/openapi.json delete /v1/files/{name}
Delete one uploaded file by name. The document drops out of workspace search after the next indexing run (one is triggered automatically).
# Get indexing run status
Source: https://docs.trycaesar.com/api-reference/files/get-indexing-run-status
/api-reference/openapi.json get /v1/files/index/{sync_id}
Progress and outcome of one files indexing run.
# Index uploaded files
Source: https://docs.trycaesar.com/api-reference/files/index-uploaded-files
/api-reference/openapi.json post /v1/files/index
Start an indexing run over the organization's uploaded files so they become searchable in the workspace index. Poll GET /v1/files/index/{sync_id} for progress.
# List uploaded files
Source: https://docs.trycaesar.com/api-reference/files/list-uploaded-files
/api-reference/openapi.json get /v1/files
List the organization's uploaded files in its Files knowledge base.
# API reference
Source: https://docs.trycaesar.com/api-reference/index
Base URL, authentication, conventions, and the endpoints of the Caesar search API.
The playground and raw API calls require an API key. [Sign up in the Caesar app](https://app.trycaesar.com), create a key, and try it now:
```bash cURL theme={"system"}
curl -s https://alpha.api.trycaesar.com/v1/search \
-H "Authorization: Bearer $CAESAR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"query": "linux kernel scheduler", "max_results": 3}'
```
## Base URL and spec
| | |
| ------------ | ----------------------------------------------------------------------------- |
| Base URL | `https://alpha.api.trycaesar.com` |
| OpenAPI spec | `GET /openapi/public.json` — stable, machine-readable, served unauthenticated |
Agents and code generators should fetch the spec directly; the generated endpoint pages alongside this one are rendered from it.
## Authentication
Send `Authorization: Bearer $CAESAR_API_KEY` on API calls. A missing or bad key returns `401` and the request does not proceed unauthenticated. See [Authentication](/authentication).
## Conventions
| Convention | Detail |
| ---------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| JSON only | Requests and responses are `application/json`. Bodies must be a single JSON object; unknown fields are rejected with `400 validation_error`; max body size is 1 MiB. |
| snake\_case | All field names are snake\_case (`doc_id`, `max_results`, `canonical_url`) — exactly as the API returns them. |
| UUIDs everywhere | Every identifier (`request_id`, `search_id`, `session_id`, `doc_id`, `passage_id`, `capture_id`, `feedback_id`) is a plain UUID string. |
| `request_id` | Every response — success or error — carries one. Quote it when reporting an issue. |
| `session_id` | If you do not supply one (`session_id` body field on `/v1/search` and `/v1/feedback`; the `X-Session-ID` header on any endpoint — `/v1/document` accepts only the header), the server issues a fresh UUID per request and echoes it. Pass it back on later calls for continuity. See [Sessions](/concepts/sessions). |
| `access` block | Success responses include your live rate-limit state. |
| `usage` block | Success responses include `requests` and `bytes_returned`. |
The `access` and `usage` blocks look like this:
```json theme={"system"}
{
"access": {
"rate_limit": { "limit_rps": 100, "remaining": 99, "reset_at": "2026-06-12T17:06:47Z" }
},
"usage": { "requests": 1, "bytes_returned": 550 }
}
```
## Endpoints
| Endpoint | Description |
| ------------------------------- | ------------------------------------------------------------------------------- |
| `POST /v1/search` | Run ranked retrieval over canonical documents and passages. |
| `POST /v1/document` | Inspect one canonical document and retrieve selected content. |
| `POST /v1/feedback` | Persist an agent or evaluation feedback event. |
| `POST /v1/files/presign` | Create a presigned upload URL for your [files knowledge base](/concepts/files). |
| `GET /v1/files` | List your organization's uploaded files. |
| `DELETE /v1/files/{name}` | Delete an uploaded file by name. |
| `POST /v1/files/index` | Start an indexing run so uploads become searchable. |
| `GET /v1/files/index/{sync_id}` | Poll an indexing run's progress. |
The [SDKs](/clients/sdks/python) and [CLI](/clients/cli/usage) wrap these exact endpoints — same fields, same semantics. The [remote MCP server](/clients/mcp/remote) exposes the search and document endpoints as its `web_search` and `web_fetch` tools; feedback and file operations are REST/SDK/CLI-only.
## Rate limits
Every response carries three headers: `X-RateLimit-Limit-RPS`, `X-RateLimit-Remaining`, and `X-RateLimit-Reset` (RFC3339). Exceeding your per-second limit returns `429 rate_limited`. There is no `Retry-After` header — wait until `X-RateLimit-Reset` or back off exponentially. Details in [Rate limits](/rate-limits).
## Errors
All errors share one envelope: `type` is the literal string `"error"`, plus `request_id` and an `error` object with a stable snake\_case `code`, a human-readable `message`, and optional `details`.
```json theme={"system"}
{
"type": "error",
"request_id": "ee314ccd-dbcd-418f-9c6a-f2917c599c67",
"error": { "code": "invalid_api_key", "message": "missing or invalid API key" }
}
```
Branch on `error.code`, not `message`. The full code table is in [Errors](/concepts/errors).
## Idempotency and caching
All three endpoints are POST and there is no idempotency key. Repeating an identical search is not guaranteed to return identical results: ordering, scores, and `search_id` values can differ between replays. `score.value` is response-local — comparable only within the response that returned it, never across responses or ranker versions. Treat each response as a snapshot, and use `doc_id` plus [provenance](/concepts/provenance) when you need reusable references.
# Search
Source: https://docs.trycaesar.com/api-reference/search/search
/api-reference/openapi.json post /v1/search
Run ranked retrieval over canonical documents and passages.
# Get organization usage
Source: https://docs.trycaesar.com/api-reference/usage/get-organization-usage
/api-reference/openapi.json get /v1/usage
Usage analytics for the API key's organization: request and error totals, latency, per-key and per-endpoint breakdowns, and billable product spend. Defaults to the last 30 days in day buckets (UTC).
# Authentication
Source: https://docs.trycaesar.com/authentication
Bearer API keys, scopes, and key-handling conventions for the Caesar API.
Caesar uses standard Bearer authentication. [Sign up in the Caesar app](https://app.trycaesar.com), create an API key, and set it in the `Authorization` header:
```bash theme={"system"}
curl -s https://alpha.api.trycaesar.com/v1/search \
-H "Authorization: Bearer $CAESAR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"query": "rust async runtime comparison", "max_results": 3}'
```
For a limited time, all accounts receive a \$1,000 starting credit grant after signup.
## API keys
| Property | API key behavior |
| --------------------------- | ----------------------------------------------------- |
| Setup | Create in the [Caesar app](https://app.trycaesar.com) |
| Rate limit | per-key limit, default 100 requests/second |
| Account attribution | recorded to your account |
| Document access by `doc_id` | account-granted documents |
An API key controls four things:
1. **Usage and credits.** Calls are attributed to the account or team that owns the key.
2. **A dedicated rate limit.** Requests are counted per key, not per IP. See [rate limits](/rate-limits).
3. **Account-scoped attribution.** Keyed searches are recorded to your account, and `/v1/feedback` referencing a `search_id` or `doc_id` is checked against that ownership. Your feedback provably belongs to your searches.
4. **Document grants.** `/v1/document` lookups by `doc_id` resolve documents your account has been granted — by appearing in your search results or by a prior direct `canonical_url` lookup.
## Getting a key
Create keys in the [Caesar app](https://app.trycaesar.com). Store the secret once; the full key is only shown at creation time.
## Key format
Console-issued keys start with `csk_`. Legacy alpha keys may start with `sk_live_`, followed by a 12-character `key_prefix` and a longer secret. Non-secret key prefixes are safe to display in dashboards and logs to identify a key. The full key is returned exactly once at creation and stored only as a hash; it cannot be retrieved again.
## API-key access
`csk_` keys can use either the default all-access setting or a selected set of API capabilities:
* **Default (`NULL`) access** means all-access. The key can use the capabilities available today and automatically receives capabilities added in the future.
* **Selected access** limits the key to the capabilities selected for it. That selection is frozen; future capabilities are not added automatically.
The existing `admin`/`member` setting controls console key-management visibility. It is not endpoint authority: API access is determined by the key's capability scopes.
| Capability group | Scope | Includes |
| ------------------ | --------------------------- | --------------------------------------------------- |
| Search | `search:read` | Search, integration results, and MCP `web_search` |
| Documents | `document:read` | Document reads and MCP `web_fetch` |
| Feedback | `feedback:write` | Feedback writes |
| Deep research runs | `research:write` | Starting, listing, and reading research run results |
| Files | `files:read`, `files:write` | Files knowledge-base read and write operations |
| Usage | `usage:read` | Usage reads |
Calling an endpoint, or an MCP tool, that requires a capability your key does not have returns the existing `403 insufficient_scope` response:
```json theme={"system"}
{
"type": "error",
"request_id": "ee314ccd-dbcd-418f-9c6a-f2917c599c67",
"error": { "code": "insufficient_scope", "message": "API key is not allowed to call this endpoint" }
}
```
## Invalid keys are rejected
Every API request must authenticate. A missing, malformed, unknown, expired, or revoked key returns `401`:
```json theme={"system"}
{
"type": "error",
"request_id": "ee314ccd-dbcd-418f-9c6a-f2917c599c67",
"error": { "code": "invalid_api_key", "message": "missing or invalid API key" }
}
```
## One environment variable everywhere
Every first-party client reads `CAESAR_API_KEY`:
```bash theme={"system"}
# set in your shell profile, or inject it from your secret manager
export CAESAR_API_KEY="$(your-secret-manager get caesar-api-key)"
```
The [Python SDK](/clients/sdks/python) and [TypeScript SDK](/clients/sdks/typescript) pick it up automatically in their constructors, the [CLI](/clients/cli/usage) resolves it after the `--key` flag, and the [MCP server](/clients/mcp/remote) accepts it as a Bearer header. If the variable is unset and no key is stored in client config, API calls fail with `401 missing_api_key`.
## Client attribution: X-Caesar-Client
First-party clients send an attribution header of the form `X-Caesar-Client: cli/0.2.0` (or `python-sdk/0.2.0`, `ts-sdk/0.2.0`). It is a convention, not a contract: the server does not validate or act on it. If you build on the raw API, sending `X-Caesar-Client: yourtool/1.2.3` is encouraged because it helps the Caesar team understand client traffic.
## Key hygiene
Never paste an API key into a chat, an issue, or a commit. Keys belong in `CAESAR_API_KEY` or your secret manager. The CLI stores keys safely via `caesar-search auth login` — the browser flow mints a named, revocable key straight into the OS keychain, `--device` covers SSH/headless machines, and `--key -` pipes one from a secret manager — and masks them in all output. If a key leaks, contact the Caesar team to revoke it — the key itself cannot be recovered or rotated in place.
# Scripting and CI
Source: https://docs.trycaesar.com/clients/cli/automation
Use the caesar-search CLI in scripts, schedulers, and CI pipelines: JSON-only parsing, exit-code control flow, configuration precedence, and the raw API escape hatch.
The CLI is built to be scripted: `--json` gives a single JSON object on stdout, errors are a JSON envelope on stderr, and exit codes are a stable contract.
## Branch on exit codes, parse only JSON
```bash theme={"system"}
#!/usr/bin/env bash
set -u
caesar-search search "zero-downtime postgres schema migration" \
--format compact --json -o /tmp/results.json
code=$?
case "$code" in
0) jq -r '.results[].doc_id' /tmp/results.json ;;
3) echo "auth error: set CAESAR_API_KEY or run caesar-search auth login" >&2; exit 1 ;;
4) echo "API error after retries; see the stderr envelope" >&2; exit 1 ;;
5) echo "timeout: retry or raise --timeout" >&2; exit 1 ;;
*) echo "bad input (exit $code): fix the command" >&2; exit 1 ;;
esac
```
This example bakes in the three scripting rules:
* **`--json` on every call.** Human output is not a stable interface — its wording and layout can change between releases. Errors arrive on stderr as `{"error": {"code": "...", "message": "...", "hint": "..."}}` (`hint` only when present); all fields are snake\_case.
* **Exit codes are the contract**, not output text: `0` success, `2` bad input, `3` auth, `4` API error, `5` timeout. The full table is on [CLI usage](/clients/cli/usage).
* **`-o` for artifact capture.** It writes the payload to the file and suppresses stdout entirely, so nothing competes with the file and output limits can't truncate JSON mid-parse. Upload the file as a CI artifact, parse it with `jq`.
## Configuration precedence
Resolution order, highest first:
| Setting | Order |
| -------- | ---------------------------------------------------------------------------------------------------------------------------- |
| API key | `--key` flag → `CAESAR_API_KEY` env → OS keychain (written by browser login) → config file `api_key` → auth error if missing |
| Base URL | `--base-url` flag → `CAESAR_BASE_URL` env → config file `base_url` → default `https://alpha.api.trycaesar.com` |
The general rule is flag over env over config file over built-in default. In CI, prefer a masked `CAESAR_API_KEY` secret over the config file — nothing to provision on disk, and `caesar-search auth status --json` will confirm `key_source` is `env`.
## Let one layer own retries
By default the CLI retries HTTP 429 and 5xx up to 3 times (4 attempts total) with exponential backoff capped at 8 seconds. Caesar rate limits use `X-RateLimit-Reset`; if a numeric `Retry-After` header is present, the CLI honors it. If your scheduler or workflow engine already handles retries, disable the CLI's so attempts aren't multiplied:
```bash theme={"system"}
caesar-search search "site reliability postmortem templates" --json --no-retry
```
With `--no-retry`, a 429 or 5xx fails immediately with exit 4 and the error envelope on stderr — your outer retry policy takes it from there.
## The `api` escape hatch
`caesar-search api ` makes an authenticated raw call against the API — same key resolution, retries, and timeout — for endpoints the CLI doesn't wrap:
```bash theme={"system"}
echo '{"query":"hello","max_results":3}' | caesar-search api POST /v1/search --input -
caesar-search api GET /skills
```
Methods: `GET`, `POST`, `PUT`, `PATCH`, `DELETE`. The path must start with `/`. `--input ` (or `--input -` for stdin) supplies a JSON body. Output is always JSON — `api` ignores `--json` — and still honors `-o`. The endpoint catalog lives in the [API reference](/api-reference/index).
## Pin versions; check for drift
Auto-updating mid-pipeline makes builds unreproducible. Instead:
* Pin the CLI in CI: `npm install -g caesar-search-cli@0.2.0`, or fetch a specific release archive and verify it against `checksums.txt`.
* Detect drift with the read-only check and surface it — don't act on it automatically:
```bash theme={"system"}
caesar-search update --check --json
# {"current": "0.1.5", "latest": "0.2.0", "update_available": true, "channel": "npm"}
```
* Upgrade deliberately with `caesar-search update`; it picks the right channel (npm, brew, standalone). Never `npm update -g` or `brew upgrade` by hand.
`update --check` works on every channel, including source (`dev`) builds. Only the actual update refuses on `dev` builds (exit 2).
## For agents
* Always pass `--json`; parse stdout only. Errors are a JSON envelope on stderr.
* Branch on exit codes (`0`/`2`/`3`/`4`/`5`), never on output text.
* Prefer `-o /tmp/.json` then `cat` the file — `-o` suppresses stdout so harness output limits can't truncate JSON.
* Exit 3 means auth: set `CAESAR_API_KEY` or run `caesar-search auth login` before retrying.
* Pass `--no-retry` only when your own loop handles retries; otherwise let the CLI back off.
Full common-mistakes table: [CLI usage](/clients/cli/usage).
# Install the CLI
Source: https://docs.trycaesar.com/clients/cli/install
Install the caesar-search CLI with Homebrew, npm, or the standalone installer, then authenticate and keep it up to date.
The CLI binary is `caesar-search`. Install it, then authenticate with an API key from the [Caesar app](https://app.trycaesar.com). For a limited time, all accounts receive a \$1,000 starting credit grant.
## Install
```bash Homebrew theme={"system"}
brew install caesar-data/tap/caesar-search
```
```bash npm theme={"system"}
npm install -g caesar-search-cli
```
```bash Installer script theme={"system"}
curl -fsSL https://raw.githubusercontent.com/caesar-data/caesar-search-cli/main/install.sh | bash
```
| Channel | Notes |
| ---------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Homebrew | A formula (not a cask) in the `caesar-data/homebrew-tap` repo. |
| npm | Package `caesar-search-cli`, binary `caesar-search`. Requires Node 20 or newer. No postinstall scripts, so `--ignore-scripts` works. |
| Installer script | Verifies the sha256 checksum against the release `checksums.txt`, then installs to `~/.local/bin` (override with `CAESAR_INSTALL_DIR`). macOS and Linux, x64 and arm64. |
Prebuilt archives for all platforms (including Windows), with checksums and SBOMs, are on [GitHub Releases](https://github.com/caesar-data/caesar-search-cli/releases).
## Verify
```bash theme={"system"}
caesar-search version
caesar-search search "hello world" --max-results 1
```
`version` prints `caesar-search 0.2.0 (, )`; with `--json` it returns `{"version": ..., "commit": ..., "build_date": ...}`. Search requires `CAESAR_API_KEY` or a key stored with `caesar-search auth login` (browser login).
## Authenticate
The default is browser login — no key ever touches your clipboard:
```bash theme={"system"}
caesar-search auth login # opens the browser; stores a named, revocable key in the OS keychain
caesar-search auth login --device # SSH/containers: approve with a short code on any device
```
Browser login creates a key labelled `CLI · · ` that you can see and revoke in the console at any time. It lands in the OS keychain (macOS Keychain, libsecret on Linux) with a 0600 config-file fallback; `--insecure-storage` forces the file.
For CI and scripts, set the `CAESAR_API_KEY` environment variable, or pipe a key in directly:
```bash theme={"system"}
caesar-search auth login --key - # reads the key from stdin, e.g. piped from a secret manager
```
Never paste an API key into a chat or leave it in shell history. Prefer browser login; for raw keys, pipe them in with `--key -`. The CLI stores file-based keys with mode 0600, masks them in all output, and never logs them.
Check what the CLI is using:
```bash theme={"system"}
caesar-search auth status --json
```
The payload reports `key_present`, `key_source` (`flag`, `env`, `keychain`, `config`, or `none`), `key_masked`, `base_url`, `api_reachable`, and `config_path`. `caesar-search auth logout` removes the stored key. Full resolution order is on [Scripting and CI](/clients/cli/automation).
## Update
```bash theme={"system"}
caesar-search update # upgrade in place
caesar-search update --check --json # report only, change nothing
```
`update` detects how the CLI was installed and uses the matching channel:
| Channel | What `update` does |
| ------------ | ---------------------------------------------------------------------------------------------------------------------- |
| `npm` | Runs `npm install -g caesar-search-cli@latest` |
| `brew` | Runs `brew upgrade caesar-data/tap/caesar-search` |
| `standalone` | Downloads the release archive, verifies its sha256 against `checksums.txt`, and atomically replaces the running binary |
| `dev` | Refuses (exit 2) — source builds update from source: `git pull && bun install` |
`--check` works on every channel, including `dev`, and returns:
```json theme={"system"}
{"current": "0.2.0", "latest": "0.2.0", "update_available": false, "channel": "brew"}
```
Don't run `npm update -g` or `brew upgrade` by hand — `caesar-search update` picks the right channel itself. If a flag errors as unknown, the installed CLI is outdated: run `caesar-search update` and retry.
## Shell completions
`caesar-search completion bash|zsh|fish` prints a completion script to stdout. Wire it up from your shell rc file:
```bash bash (~/.bashrc) theme={"system"}
eval "$(caesar-search completion bash)"
```
```bash zsh (~/.zshrc) theme={"system"}
eval "$(caesar-search completion zsh)"
```
```bash fish (~/.config/fish/config.fish) theme={"system"}
caesar-search completion fish | source
```
## For agents
* The binary is `caesar-search`, not `caesar`.
* API calls require a key — verify `CAESAR_API_KEY` or run `caesar-search auth login` before search/read tasks.
* `npm install -g caesar-search-cli --ignore-scripts` is safe; there are no postinstall scripts.
* On an unknown-flag error, run `caesar-search update`, then retry the command.
* Never echo or log a key; `auth login --key -` reads it from stdin.
Full command reference and the common-mistakes table: [CLI usage](/clients/cli/usage).
# CLI usage
Source: https://docs.trycaesar.com/clients/cli/usage
Command reference for the caesar-search CLI: search, read, feedback, JSON output, and the exit-code contract.
The standard loop is search, pick a `doc_id`, read it, optionally send feedback:
```bash theme={"system"}
caesar-search search "kubernetes operator patterns" --max-results 5 --format compact --json -o /tmp/search.json
DID=$(jq -r '.results[0].doc_id' /tmp/search.json)
SID=$(jq -r '.search_id' /tmp/search.json)
caesar-search read "$DID" --query "reconciliation loop" --json
caesar-search feedback --event-type result_helpful --search-id "$SID" --doc-id "$DID"
```
Keep returned source URLs and IDs (`doc_id`, `search_id`, `capture_id`, `canonical_url`) when you need to audit or cite a result. See [provenance](/concepts/provenance).
## Commands
| Command | Does | Key flags |
| ------------------------------------ | ------------------------------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------- |
| `search ` | Web search | `--max-results`, `--format` |
| `read ` | Read a page as markdown (aliases: `fetch`, `extract`) | `--query`, `--max-chars`, `--start-char`, `--include` |
| `feedback` | Send result feedback | `--event-type` (required), `--search-id`, `--doc-id`, `--passage-id`, `--query`, `--rank`, `--notes` |
| `files upload ` | Upload to your [files knowledge base](/concepts/files) and index | `--no-index`, `--content-type` |
| `files list\|delete\|index\|status` | Manage uploads and indexing runs | `--mode` (index) |
| `usage` | Your organization's [API usage](/api-reference): requests, errors, latency, spend | `--from`, `--to`, `--interval`, `--key-id` |
| `auth status\|login\|logout` | Manage the stored API key; `login` opens the browser | `--key`, `--device`, `--no-open`, `--insecure-storage` |
| `config get\|set\|unset\|list\|path` | Manage config keys `api_key`, `base_url`, `oauth_issuer`, `oauth_client_id`, `console_url` | — |
| `api ` | Raw authenticated API call (escape hatch) | `--input ` |
| `update` | Channel-aware self-update | `--check` |
| `version` | Print version, commit, and build date | — |
| `completion ` | Completion script for `bash`, `zsh`, or `fish` | — |
## Global flags
Available on every command:
| Flag | Meaning | Default |
| --------------------- | ------------------------------------------------------------- | --------------------------------- |
| `--json` | Machine output: data on stdout, JSON error envelope on stderr | off (human output) |
| `-o, --output ` | Write data output to a file and suppress stdout | — |
| `--key ` | API key (overrides `CAESAR_API_KEY` and the config file) | — |
| `--base-url ` | API base URL (overrides `CAESAR_BASE_URL`) | `https://alpha.api.trycaesar.com` |
| `--no-retry` | Disable rate-limit/5xx retries | retries on |
| `--timeout ` | Request timeout in seconds | `30` |
`--format` is not a global flag — it belongs to `search` only.
## search
| Flag | Values | Default |
| --------------- | ----------------------------------------- | ---------- |
| `--max-results` | integer 1–50 | `10` |
| `--format` | `ids_only`, `compact`, `standard`, `full` | `standard` |
`--format` maps to the API's [response shaping](/concepts/response-shaping) verbosity: `compact` is the token-efficient choice for search-then-read loops, `standard` adds quotable passages, `full` adds capture provenance.
## read
The target should be a full URL (`https://…`) or a `doc_id` UUID; a scheme-less target (e.g. `example.com`) is normalized to `https://` by the API.
| Flag | Values | Default |
| -------------- | --------------------------------------------------------- | ------------------ |
| `--query` | query context for the read | — |
| `--max-chars` | optional content character cap, 1–50000 | — |
| `--start-char` | resume a truncated read from this offset | `0` |
| `--include` | comma list of `metadata,content,passages,capture_history` | `metadata,content` |
A truncated read reports `content.truncated: true` plus `content.char_count` and `content.start_char` (omitted when 0). Continue with `--start-char` set to start plus count — don't retry with a bigger `--max-chars`:
```bash theme={"system"}
caesar-search read https://en.wikipedia.org/wiki/Web_crawler --max-chars 4000 --json
# content.truncated: true, content.char_count: 4000 (content.start_char omitted when 0)
caesar-search read https://en.wikipedia.org/wiki/Web_crawler --start-char 4000 --json
```
## feedback
`--event-type` is required. Values: `result_helpful`, `result_not_helpful`, `passage_used`, `read_abandoned`, `duplicate_result`, `stale_result`, `spam_or_low_quality`, `missing_expected_source`, `unsafe_or_policy_issue`. Pair it with `--search-id` and/or `--doc-id` so the event lands on the right target.
## files
Upload documents into your organization's [files knowledge base](/concepts/files) and search them with the workspace scope:
```bash theme={"system"}
caesar-search files upload ./q3-report.pdf # presign → upload → auto-index
caesar-search files upload a.md b.csv --no-index # batch now…
caesar-search files index # …index once
caesar-search files status # poll the run
caesar-search files list
caesar-search files delete "q3-report.pdf"
```
The upload PUTs the bytes straight to storage via a presigned URL — the API key is never sent to storage. `--mode full` on `files index` reprocesses everything instead of just new/changed files.
## usage
See your organization's API usage rendered in the terminal — headline totals, a request sparkline, per-endpoint and per-key tables, and billable spend:
```bash theme={"system"}
caesar-search usage # last 30 days, day buckets
caesar-search usage --from 2026-07-16 --interval hour
caesar-search usage --key-id # one key only; repeat the flag for several
caesar-search usage --json | jq .headline
```
Notes:
* All numbers are scoped to the organization that owns the API key. The data matches the console's usage dashboard.
* Spend fields are `spend_cents` (exact, possibly fractional cents). The human view formats them as dollars.
* The window is at most 92 days; `--interval hour` needs a range of 8 days or less.
* `--key` sets the API key; the per-key filter is `--key-id`.
## Reading from stdin
`-` reads stdin to EOF for the `search` query, the `read` target, `auth login --key -`, and `api --input -`:
```bash theme={"system"}
echo "model context protocol spec" | caesar-search search - --json | jq -r '.results[0].doc_id'
```
## JSON output
With `--json`, data is a single JSON object on stdout and errors are a JSON envelope on stderr (`hint` only when present):
```json theme={"system"}
{"error": {"code": "rate_limited", "message": "rate limit exceeded", "hint": "Rate limited. Wait and retry, or lower request volume."}}
```
All fields are snake\_case, exactly as the API returns them. Human (non-`--json`) output is not a stable interface — never parse it.
`-o ` writes the data payload to the file and suppresses stdout entirely, so output limits can't truncate JSON mid-parse. The agent pattern is write-then-read:
```bash theme={"system"}
caesar-search search "rust async runtimes compared" --format compact --json -o /tmp/out.json
cat /tmp/out.json
```
Errors still go to stderr, and the exit code is unchanged.
## Exit codes
| Code | Meaning |
| ---- | ---------------------------------------------------------------- |
| 0 | success (includes `--help` and `--version`) |
| 2 | bad input (validation errors, unknown flags, usage errors) |
| 3 | auth error (HTTP 401/403) |
| 4 | API error (other HTTP errors, network failures, update failures) |
| 5 | timeout |
Branch on these, not on output parsing — see [Scripting and CI](/clients/cli/automation).
## For agents
| Mistake | Correction |
| --------------------------------------------------- | ----------------------------------------------------------------------- |
| `--results`, `--limit`, `-n` | The flag is `--max-results` |
| `--response-format` | The flag is `--format` (values: ids\_only, compact, standard, full) |
| Passing a bare domain to `read` | `read` needs a full URL (`https://…`) or a `doc_id` UUID |
| Retrying a truncated read with bigger `--max-chars` | Use `--start-char` to continue from where it stopped |
| Parsing human output | Use `--json`; human output is not a stable interface |
| Shell-redirecting and also reading stdout | Use `-o `; it suppresses stdout so nothing competes with the file |
| `npm update -g` / `brew upgrade` by hand | `caesar-search update` picks the right channel itself |
| Expecting camelCase JSON | All fields are snake\_case, exactly as the API returns them |
| `caesar search` | The binary is `caesar-search` |
# Agent Development Kit
Source: https://docs.trycaesar.com/clients/integrations/adk
Use Caesar as function tools or MCP tools in Agent Development Kit projects.
Agent Development Kit projects usually need only two Caesar tools: search and read. If your runtime can consume MCP tools, connect the [remote MCP server](/clients/mcp/remote). If you prefer local function tools, wrap the Python SDK:
```bash theme={"system"}
pip install caesar-search
```
## Function implementations
```python theme={"system"}
from typing import Any
from caesar_search import Caesar
client = Caesar() # reads CAESAR_API_KEY
def web_search(query: str, max_results: int = 5) -> dict[str, Any]:
"""Search the web with Caesar and return compact, citable results."""
response = client.search(query, max_results=max_results, verbosity="compact")
return response.model_dump()
def web_fetch(target: str, query: str | None = None, max_chars: int | None = None) -> dict[str, Any]:
"""Read a Caesar doc_id or URL as clean markdown."""
response = client.read(target, query=query, max_chars=max_chars)
return response.model_dump()
CAESAR_TOOLS = [web_search, web_fetch]
```
Register `CAESAR_TOOLS` with your kit's function-tool helper. Use `web_search` and `web_fetch` when you want the model to treat Caesar as its default web-search provider.
## MCP alternative
Use the hosted streamable-HTTP endpoint when the kit supports MCP:
```text theme={"system"}
https://alpha.api.trycaesar.com/mcp?profile=web-search
```
Send `Authorization: Bearer $CAESAR_API_KEY` in the MCP client config.
## Tool behavior
| Tool | Purpose | Default |
| ------------ | ------------------------------------------------- | ---------------------- |
| `web_search` | Find ranked documents and return `doc_id` handles | `compact` result shape |
| `web_fetch` | Read a `doc_id` or URL as markdown | 12,000 character cap |
Feedback is not exposed over MCP. For local function tools, keep the SDK client available and call `client.feedback(...)` after the agent acts on a result.
## For agents
* Prefer the remote MCP server when your kit supports it. It keeps tool schemas aligned with the deployed API.
* If you wrap local functions, return full SDK model dumps instead of hand-shaped strings. Agents need `doc_id`, `search_id`, and truncation fields.
* Use one search, then read selected documents. Repeated broad searches waste rate-limit tokens and context.
* Never pass a literal key through an agent prompt. Use `CAESAR_API_KEY`.
# CrewAI
Source: https://docs.trycaesar.com/clients/integrations/crewai
Create CrewAI tools that expose Caesar search and document reading to multi-agent workflows.
Install Caesar beside your CrewAI project:
```bash theme={"system"}
pip install caesar-search crewai pydantic
```
## Search tool
```python theme={"system"}
from typing import Type
from caesar_search import Caesar
from crewai.tools import BaseTool
from pydantic import BaseModel, Field
client = Caesar() # reads CAESAR_API_KEY
class CaesarSearchInput(BaseModel):
query: str = Field(..., description="Search query")
max_results: int = Field(5, description="Maximum results, 1-50")
class CaesarSearchTool(BaseTool):
name: str = "web_search"
description: str = "Search the web with Caesar and return ranked results with doc_id values."
args_schema: Type[BaseModel] = CaesarSearchInput
def _run(self, query: str, max_results: int = 5) -> str:
response = client.search(query, max_results=max_results, verbosity="compact")
return response.model_dump_json()
```
## Read tool
```python theme={"system"}
class CaesarReadInput(BaseModel):
target: str = Field(..., description="Caesar doc_id or URL")
query: str | None = Field(None, description="Optional focus question")
max_chars: int | None = Field(None, description="Optional maximum characters to return")
class CaesarReadTool(BaseTool):
name: str = "web_fetch"
description: str = "Read a Caesar doc_id or URL as clean markdown."
args_schema: Type[BaseModel] = CaesarReadInput
def _run(self, target: str, query: str | None = None, max_chars: int | None = None) -> str:
response = client.read(target, query=query, max_chars=max_chars)
return response.model_dump_json()
tools = [CaesarSearchTool(), CaesarReadTool()]
```
Attach `tools` to the agents that need web retrieval. For a research crew, give search to the planning agent and read to the evidence-gathering agent, or give both tools to a single retrieval specialist.
## Feedback
Crew handoffs can lose the `search_id` unless you preserve it. Store it with the chosen `doc_id` and send feedback after the task is complete:
```python theme={"system"}
client.feedback("result_helpful", search_id=search_id, doc_id=doc_id)
```
## For agents
* Keep the search result JSON available to the crew. It contains `search_id` for feedback and `doc_id` for reading.
* Do not ask every agent to search independently. Let one retrieval agent search, then hand off selected `doc_id` values.
* Use `web_fetch` for evidence before making claims. Snippets are for triage; documents are for citations.
* If your crew runtime supports MCP, the [remote MCP server](/clients/mcp/remote) is the lowest-maintenance integration.
# LangChain
Source: https://docs.trycaesar.com/clients/integrations/langchain
Wrap Caesar's Python or TypeScript client as LangChain tools for search and document reading.
Install Caesar beside the framework packages your agent already uses:
```bash Python theme={"system"}
pip install caesar-search langchain-core
```
```bash TypeScript theme={"system"}
npm install caesar-search @langchain/core zod
```
## Python tools
```python theme={"system"}
from typing import Any
from caesar_search import Caesar
from langchain_core.tools import tool
client = Caesar() # reads CAESAR_API_KEY
@tool
def web_search(query: str, max_results: int = 5) -> dict[str, Any]:
"""Search the web with Caesar and return compact, citable results."""
response = client.search(query, max_results=max_results, verbosity="compact")
return {
"search_id": response.search_id,
"results": [
{
"rank": result.rank,
"title": result.title,
"url": result.canonical_url,
"doc_id": result.doc_id,
"snippet": result.snippet,
}
for result in response.results or []
],
}
@tool
def web_fetch(target: str, query: str | None = None, max_chars: int | None = None) -> dict[str, Any]:
"""Read a Caesar doc_id or URL as clean markdown."""
response = client.read(target, query=query, max_chars=max_chars)
return {
"doc_id": response.doc.doc_id,
"url": response.doc.canonical_url,
"title": response.doc.title,
"content": response.content.text if response.content else "",
"truncated": response.content.truncated if response.content else False,
}
tools = [web_search, web_fetch]
```
Pass `tools` into your agent or graph the same way you pass any other framework tool. Keep search and read separate: the first tool finds candidates cheaply, the second spends context only on the document the agent chooses.
## TypeScript tools
```ts theme={"system"}
import { tool } from "@langchain/core/tools";
import { z } from "zod";
import { Caesar } from "caesar-search";
const caesar = new Caesar(); // reads CAESAR_API_KEY
export const caesarSearch = tool(
async ({ query, max_results }) => {
const response = await caesar.search(query, {
maxResults: max_results ?? 5,
verbosity: "compact",
});
return {
search_id: response.search_id,
results: (response.results ?? []).map((result) => ({
rank: result.rank,
title: result.title,
url: result.canonical_url,
doc_id: result.doc_id,
snippet: result.snippet,
})),
};
},
{
name: "web_search",
description: "Search the web with Caesar and return compact, citable results.",
schema: z.object({
query: z.string(),
max_results: z.number().int().min(1).max(50).optional(),
}),
},
);
export const caesarRead = tool(
async ({ target, query, max_chars }) => {
const response = await caesar.read(target, {
query,
maxChars: max_chars,
});
return {
doc_id: response.doc?.doc_id,
url: response.doc?.canonical_url,
title: response.doc?.title,
content: response.content?.text ?? "",
truncated: response.content?.truncated ?? false,
};
},
{
name: "web_fetch",
description: "Read a Caesar doc_id or URL as clean markdown.",
schema: z.object({
target: z.string(),
query: z.string().optional(),
max_chars: z.number().int().min(1).max(50000).optional(),
}),
},
);
export const tools = [caesarSearch, caesarRead];
```
## Feedback
Framework tool loops usually do not call feedback automatically. After the agent uses a result, call the SDK directly with the `search_id` and `doc_id` you retained from the search response:
```python theme={"system"}
client.feedback("result_helpful", search_id=search_id, doc_id=doc_id)
```
## For agents
* Use `web_search` first with `compact` results, then call `web_fetch` only for the `doc_id` you actually need.
* Preserve `doc_id` and `search_id` exactly. They are opaque IDs, not strings to shorten or reconstruct.
* Do not merge search and read into one giant tool. Two smaller tools give the agent better control over context cost.
* If your runtime supports MCP, the [remote MCP server](/clients/mcp/remote) exposes `web_search` and `web_fetch` without custom code.
# LlamaIndex
Source: https://docs.trycaesar.com/clients/integrations/llamaindex
Use Caesar as LlamaIndex function tools for search-first, read-second retrieval workflows.
Install Caesar with the LlamaIndex package set you already use:
```bash theme={"system"}
pip install caesar-search llama-index
```
## Function tools
```python theme={"system"}
from caesar_search import Caesar
from llama_index.core.tools import FunctionTool
client = Caesar() # reads CAESAR_API_KEY
def web_search(query: str, max_results: int = 5) -> str:
"""Search the web with Caesar and return compact JSON."""
response = client.search(query, max_results=max_results, verbosity="compact")
return response.model_dump_json()
def web_fetch(target: str, query: str | None = None, max_chars: int | None = None) -> str:
"""Read a Caesar doc_id or URL as markdown plus metadata."""
response = client.read(target, query=query, max_chars=max_chars)
return response.model_dump_json()
web_search_tool = FunctionTool.from_defaults(
fn=web_search,
name="web_search",
description="Search the web with Caesar. Returns ranked results with doc_id values.",
)
web_fetch_tool = FunctionTool.from_defaults(
fn=web_fetch,
name="web_fetch",
description="Read a Caesar doc_id or URL as clean markdown.",
)
tools = [web_search_tool, web_fetch_tool]
```
Pass `tools` into your agent workflow. Keep the JSON response intact unless you have a reason to trim it; the `doc_id`, `search_id`, and provenance fields are the useful part of the result.
## Search, then read
The strongest pattern is a two-step loop:
1. `web_search(query, max_results=5)` to find candidates.
2. Pick the most relevant `doc_id`.
3. `web_fetch(doc_id, query="what the agent needs")` to fetch clean markdown.
4. Send feedback through the SDK if a result helped.
```python theme={"system"}
results = client.search("postgres 17 logical replication failover", max_results=5)
doc_id = results.results[0].doc_id
doc = client.read(doc_id, query="failover behavior")
client.feedback("result_helpful", search_id=results.search_id, doc_id=doc_id)
```
## For agents
* Return JSON from the tool when possible. It preserves snake\_case fields and prevents accidental loss of `doc_id`.
* Prefer `verbosity="compact"` for the search tool. Read the selected document for longer evidence.
* On truncated reads, continue with the returned `start_char` value instead of asking for a larger `max_chars`.
* If your LlamaIndex runtime can consume MCP tools directly, use the [remote MCP server](/clients/mcp/remote) and skip custom wrappers.
# Tool calling
Source: https://docs.trycaesar.com/clients/integrations/tool-calling
Expose Caesar search and read as generic function tools in any model runtime that supports tool calls.
Any tool-calling runtime can use Caesar with two functions: `web_search` and `web_fetch`. The same tool names appear in the [MCP server](/clients/mcp/remote) and [AI SDK tools](/clients/sdks/ai-sdk).
## TypeScript dispatcher
```bash theme={"system"}
npm install caesar-search
```
```ts theme={"system"}
import { Caesar } from "caesar-search";
const caesar = new Caesar(); // reads CAESAR_API_KEY
export const caesarTools = [
{
name: "web_search",
description: "Search the web with Caesar and return ranked results with doc_id values.",
input_schema: {
type: "object",
properties: {
query: { type: "string" },
max_results: { type: "integer", minimum: 1, maximum: 50 },
response_format: { type: "string", enum: ["compact", "standard", "full"] },
},
required: ["query"],
},
},
{
name: "web_fetch",
description: "Read a Caesar doc_id or URL as clean markdown.",
input_schema: {
type: "object",
properties: {
target: { type: "string" },
query: { type: "string" },
max_chars: { type: "integer", minimum: 1, maximum: 50000 },
start_char: { type: "integer", minimum: 0 },
},
required: ["target"],
},
},
];
export async function runCaesarToolCall(call: {
name: string;
input: Record;
}) {
if (call.name === "web_search") {
const query = String(call.input.query);
const maxResults = Number(call.input.max_results ?? 8);
const responseFormat = String(call.input.response_format ?? "compact");
return caesar.search(query, {
maxResults,
verbosity: responseFormat as "compact" | "standard" | "full",
});
}
if (call.name === "web_fetch") {
const target = String(call.input.target);
return caesar.read(target, {
query: call.input.query ? String(call.input.query) : undefined,
maxChars:
call.input.max_chars === undefined
? undefined
: Number(call.input.max_chars),
startChar: Number(call.input.start_char ?? 0),
});
}
throw new Error(`unknown tool: ${call.name}`);
}
```
Adapt the schema wrapper (`input_schema`, `parameters`, or similar) to your model runtime, but keep the inner field names snake\_case. Those names match the API and reduce translation errors.
## Python dispatcher
```bash theme={"system"}
pip install caesar-search
```
```python theme={"system"}
from typing import Any
from caesar_search import Caesar
client = Caesar() # reads CAESAR_API_KEY
def run_caesar_tool_call(name: str, arguments: dict[str, Any]) -> Any:
if name == "web_search":
return client.search(
str(arguments["query"]),
max_results=int(arguments.get("max_results", 8)),
verbosity=str(arguments.get("response_format", "compact")),
).model_dump()
if name == "web_fetch":
max_chars = arguments.get("max_chars")
return client.read(
str(arguments["target"]),
query=arguments.get("query"),
max_chars=int(max_chars) if max_chars is not None else None,
start_char=int(arguments.get("start_char", 0)),
).model_dump()
raise ValueError(f"unknown tool: {name}")
```
## Feedback
Tool-calling loops often stop after the model produces an answer. If you can observe which result the agent used, close the loop:
```ts theme={"system"}
await caesar.feedback("result_helpful", {
searchId: search_id,
docId: doc_id,
});
```
## For agents
* Use `response_format: "compact"` for initial searches. Request `standard` or `full` only when you need passages or capture provenance.
* Pass `doc_id` from `web_search` into `web_fetch`. Prefer it over reusing the URL when you have it.
* Preserve `search_id` until the task is done so feedback can be attributed to the right ranked list.
* Do not expose API keys as tool arguments. Configure `CAESAR_API_KEY` in the runtime environment.
# Remote MCP server
Source: https://docs.trycaesar.com/clients/mcp/remote
Connect any MCP client to Caesar's hosted streamable-HTTP server for web search and document reading.
Caesar serves a remote MCP server at `https://alpha.api.trycaesar.com/mcp` — the same host as the REST API. It is remote-only today. The server is moving to its own canonical origin, `https://mcp.trycaesar.com/mcp`, with **one-click OAuth connect** for hosts like Claude and ChatGPT; the alpha path stays up as a transparent proxy, and raw Bearer keys keep working on both hosts. Until the new origin is live in your environment, use the alpha URL shown in the examples.
To make Caesar the agent's web-search provider, connect the web-search profile. It exposes generic tool names (`web_search`, `web_fetch`) that agents naturally choose for web lookup and URL-reading tasks:
```bash theme={"system"}
claude mcp add --transport http caesar "https://alpha.api.trycaesar.com/mcp?profile=web-search" \
--header "Authorization: Bearer $CAESAR_API_KEY"
```
## Use this when
* You are in a chat or IDE client that supports remote MCP tools.
* You want search and read without installing the CLI locally.
* You want the agent to preserve `doc_id`, `search_id`, and `capture_id` across tool calls.
## Connection states
| State | What you see | Next action |
| ------------------- | ------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------- |
| Not connected | No `caesar` server in the client config | Copy the client-specific config below. |
| Connecting | Config exists, but tools are not listed | Restart or reload MCP servers in the client. |
| Connected | Tool list includes the tools allowed by the key on the web-search profile | Run a one-result search and read the best `doc_id` when those tools are available. |
| Needs authorization | The client shows a "Connect"/login prompt | Approve in the browser — OAuth-capable hosts discover the login flow from the server's 401 challenge. |
| Failed | HTTP 401, HTTP 429, or transport error | Use the matching recovery block in [Install for agents](/agents/install) or [rate limits](/rate-limits). |
## Server facts
| Fact | Value |
| ------------------ | ---------------------------------------------------------------------------------------------------------- |
| Endpoint | `https://alpha.api.trycaesar.com/mcp` |
| Web-search profile | `https://alpha.api.trycaesar.com/mcp?profile=web-search` |
| Transport | MCP streamable HTTP, stateless, plain JSON responses (no SSE required) |
| Server identity | name `caesar`, title `Caesar Search`, version `0.1.0` |
| Tools | `web_search` (`search:read`), `web_fetch` (`document:read`) (feedback is REST/CLI/SDK-only) |
| Protocol versions | `2025-11-25` (latest), `2025-06-18`, `2025-03-26`, `2024-11-05` |
| Authorization | Dual-mode: OAuth 2.1 (authorization code + PKCE `S256`, RFC 8707 audience binding) or a raw Bearer API key |
| Resource metadata | `/.well-known/oauth-protected-resource` (RFC 9728), also at the `/mcp` path-inserted form |
The server returns an `mcp-session-id` header, but because it is stateless, follow-up JSON-RPC requests work without it. If a client requests an unsupported protocol version, the server answers with its latest (`2025-11-25`).
Legacy `tools/call` requests for `caesar_search` and `caesar_read` are still accepted for existing clients, but those names are not advertised by `tools/list`.
## Connect from other clients
```bash Claude Code theme={"system"}
claude mcp add --transport http caesar "https://alpha.api.trycaesar.com/mcp?profile=web-search" \
--header "Authorization: Bearer $CAESAR_API_KEY"
```
```json Cursor / Windsurf theme={"system"}
{
"mcpServers": {
"caesar": {
"url": "https://alpha.api.trycaesar.com/mcp?profile=web-search",
"headers": { "Authorization": "Bearer $CAESAR_API_KEY" }
}
}
}
```
```bash cURL (raw JSON-RPC) theme={"system"}
curl -s https://alpha.api.trycaesar.com/mcp \
-H 'Authorization: Bearer $CAESAR_API_KEY' \
-H 'Content-Type: application/json' \
-H 'Accept: application/json, text/event-stream' \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"web_search","arguments":{"query":"linux kernel scheduler","max_results":2}}}'
```
The canonical per-client install matrix lives at [Install for agents](/agents/install). Keep the Bearer header in MCP configs and never commit a literal key.
## Authentication and rate limits
`/mcp` sits behind the same [auth](/authentication) and [rate-limit](/rate-limits) middleware as the REST endpoints, and supports **two credential types**:
* **OAuth (one-click connect).** Hosts that implement the MCP authorization spec need no key at all: an unauthenticated request gets HTTP 401 with a `WWW-Authenticate` challenge pointing at the server's protected resource metadata, the host discovers the authorization server, and the user approves in the browser (authorization code + PKCE `S256`). Access tokens are short-lived and audience-bound to the canonical `/mcp` resource — tokens minted for any other audience are rejected (`invalid_token`).
* **Raw Bearer API key.** A `csk_` or `sk_live_` key in the `Authorization` header skips OAuth entirely — unchanged for CI, Codex-style `bearer_token_env_var` configs, and every existing integration. A missing credential returns HTTP 401 (`missing_api_key`), a bad key HTTP 401 (`invalid_api_key`).
Both resolve to the same organization for billing, usage attribution, and rate limits:
* Authenticated connections default to 100 RPS.
* Scopes are enforced per tool: `web_search` requires `search:read`, and `web_fetch` requires `document:read`. A key may authenticate successfully but see no allowed tools; `tools/list` advertises only tools allowed by that key.
* Missing permission returns the existing `403 insufficient_scope` response. Invalid or missing authentication returns `401`.
* **Every JSON-RPC message counts as one rate-limited request.** Over the limit, the server returns HTTP 429 (`rate_limited`). Auth and rate-limit failures are HTTP errors, not MCP tool errors.
* Your callers' credentials never reach upstream providers — the server always uses its own provider credentials (no token passthrough).
## Tool: web\_search
Search the web and get ranked results with snippets, passages, and provenance. Each result carries a `doc_id` you can pass to `web_fetch`.
| Param | Type | Required | Default | Notes |
| ----------------- | ------- | -------- | --------- | ----------------------------------------------------------------------------------------------------------------------- |
| `query` | string | yes | — | Put constraints (site, filetype, exact phrases, recency) directly in the query text. |
| `max_results` | integer | no | 8 | 1–50. Values above 50 are clamped to 50; zero or negative values fall back to the default 8. Never errored. |
| `response_format` | string | no | `compact` | `compact` (title, url, snippet, dates), `standard` (adds passages, max 2 per result), `full` (adds capture provenance). |
The MCP default is `compact` — note this differs from the REST default of `standard`. `ids_only` is not available over MCP; it (and any unknown value) silently falls back to `compact`. See [response shaping](/concepts/response-shaping) for the shared preset table.
The result is a single text content block containing JSON: `search_id`, `results[]` with `rank`, `title`, `url`, `doc_id`, `snippet`, `score`, `published_at`, `last_crawled_at` (plus `passages` — max 2 — `source_url` when it differs from `url`, and top-level `ranker_version`/`score_scope` at `standard`+, and `capture_id`/`capture_time` at `full`), optional `warnings`, `truncated`, and `hint`.
Responses are capped at 20,000 characters. On overflow the server sets `truncated: true`, sheds all passages first, then drops trailing results (never below 1), and adds the hint:
```text theme={"system"}
Truncated to fit the response cap. Narrow the query, lower max_results, or read one document with web_fetch (start_char to continue).
```
## Tool: web\_fetch
Read a web page as clean markdown with document metadata and provenance. Accepts a `url` or a `doc_id` returned by `web_search` — providing neither is a tool error (`validation_error: provide either url or doc_id`).
| Param | Type | Required | Default | Notes |
| ------------ | ------- | ------------------ | ------- | ------------------------------------------------------------------------------------------------------ |
| `url` | string | one of url/doc\_id | — | URL of the page to read. |
| `doc_id` | string | one of url/doc\_id | — | Caesar `doc_id` from a previous `web_search` result. |
| `query` | string | no | — | Focuses content selection on the most relevant sections. |
| `max_chars` | integer | no | 12000 | Max 50,000. Values above the max are clamped; zero or negative values fall back to the default 12,000. |
| `start_char` | integer | no | 0 | Resume a truncated read from this character offset. |
The result JSON contains `doc_id`, `url`, `title`, `published_at`, `capture_time`, `start_char` (omitted when 0), `char_count`, `truncated`, `hint`, and `content` (always markdown).
When content is truncated, the hint tells you the exact next offset: `Content truncated. Continue reading with start_char=12000.` Pass that value back as `start_char` to get the next window. Continuation reads force full-document selection (a `query` alongside `start_char` does not excerpt), so offsets stay contiguous across calls.
## Scoping the tool set
Append `?tools=` to the endpoint URL to further limit which tools are advertised and callable — comma-separated, empty means all tools allowed by the key:
```text theme={"system"}
https://alpha.api.trycaesar.com/mcp?tools=web_search
```
Use `?profile=web-search` as a named profile for the same two-tool set:
```text theme={"system"}
https://alpha.api.trycaesar.com/mcp?profile=web-search
```
## Tool errors
Tool failures come back as MCP results with `isError: true` and a single text block formatted as `code: message`, where the code matches the REST [error codes](/concepts/errors) (`validation_error`, `document_not_found`, `provider_unavailable`, `internal_error`). Authentication and scope failures are HTTP responses instead: `401` for invalid authentication and `403 insufficient_scope` for missing permission.
## For agents
* The `compact` default exists to protect your context window. Stay on it for find-then-read loops; request `standard` only when you need quotable passages, `full` only when you need capture provenance.
* Thread handles: take `doc_id` from a `web_search` result and pass it to `web_fetch`. Preserve `doc_id`, `search_id`, and `capture_id` values verbatim — they are opaque identifiers, never reconstruct or abbreviate them.
* On a truncated read, continue with the `start_char` value from the hint instead of retrying with a bigger `max_chars`.
* Out-of-range `max_results` and `max_chars` are silently coerced over MCP (high values clamped to the max, non-positive values reset to the defaults) — do not expect REST-style 400s.
* Each JSON-RPC message spends one rate-limit token, so batch judgment matters: prefer one well-constrained search over several broad ones.
# Vercel AI SDK tools
Source: https://docs.trycaesar.com/clients/sdks/ai-sdk
Give any Vercel AI SDK agent Caesar search and read tools with one import.
```bash theme={"system"}
npm install caesar-search ai
```
The `ai` package (version 5 or newer) is an optional peer dependency of `caesar-search` — optional for the rest of the SDK, required for this subpath. Importing `caesar-search/ai` without it fails at module resolution.
```ts theme={"system"}
import { generateText } from "ai";
import { caesarTools } from "caesar-search/ai";
const { text } = await generateText({
model,
tools: caesarTools(),
prompt: "What changed in Postgres 17 logical replication?",
});
```
The default client reads `CAESAR_API_KEY`. Create a key in the [Caesar app](https://app.trycaesar.com) and set it in the runtime environment before the model calls these tools.
## The tools
`caesarTools()` returns two enumerable tools:
| Tool | Purpose |
| ------------ | ---------------------------------------------------------- |
| `web_search` | Search the web with ranked results and provenance handles. |
| `web_fetch` | Read a URL or `doc_id` as clean markdown. |
Use the TypeScript SDK client directly for other API actions. Legacy direct properties `caesar_search` and `caesar_read` remain available for existing code, but they are non-enumerable so they are not sent to models as tool choices.
### web\_search
| Parameter | Type | Description |
| ----------------- | --------------------------------- | ------------------------------------------------ |
| `query` | string, required | The search query. |
| `max_results` | number | Maximum results, 1-50. Default 8. |
| `response_format` | `compact` \| `standard` \| `full` | compact (default) is the token-efficient choice. |
Runs `client.search(query, { maxResults, verbosity })`. The `ids_only` verbosity is deliberately omitted from the tool's `response_format` enum — it exists at the client level, but is a poor fit for tool results. See [response shaping](/concepts/response-shaping).
### web\_fetch
| Parameter | Type | Description |
| ------------ | ---------------- | ----------------------------------------------------------- |
| `target` | string, required | URL or doc\_id to read. |
| `query` | string | Query context for the read. |
| `max_chars` | number | Optional content character cap. Omit for the full document. |
| `start_char` | number | Resume a truncated read from this character offset. |
Runs `client.read(target, { query, maxChars, startChar })`.
## Configuring the client
Pass your own configured client to control auth, timeouts, and retries:
```ts theme={"system"}
import { Caesar } from "caesar-search";
import { caesarTools } from "caesar-search/ai";
const tools = caesarTools({
client: new Caesar({ timeoutMs: 10000, maxRetries: 1 }),
});
```
All [TypeScript SDK](/clients/sdks/typescript) constructor options apply. If your agent speaks MCP instead of the AI SDK, the [remote MCP server](/clients/mcp/remote) exposes the same two tools.
## For agents
* Tool results default to `compact` to protect the context window. Ask for `standard` or `full` only when you need passages or [provenance](/concepts/provenance).
* Thread `doc_id` values between tool calls: `web_search` returns a `doc_id` per result, and it is the most reliable `target` for `web_fetch` — more stable than re-passing the URL.
# Python SDK
Source: https://docs.trycaesar.com/clients/sdks/python
Typed Python client for the Caesar API with sync and async support, automatic retries, and Pydantic v2 models.
```python theme={"system"}
# pip install caesar-search (or: uv add caesar-search)
from caesar_search import Caesar
client = Caesar() # reads CAESAR_API_KEY
results = client.search("rust async runtime comparison", max_results=5)
doc_id = results.results[0].doc_id
doc = client.read(doc_id, query="which runtime is fastest")
print(doc.content.text if doc.content else "")
```
[Sign up in the Caesar app](https://app.trycaesar.com), create an API key, and set `CAESAR_API_KEY` before running the SDK.
## Install
```bash theme={"system"}
pip install caesar-search
# or
uv add caesar-search
```
The package installs as `caesar-search` and imports as `caesar_search`. Requires Python 3.10+ with `httpx` and Pydantic v2 (installed automatically). Current version: 0.2.0, MIT licensed.
## Clients
`Caesar` is the synchronous client. `AsyncCaesar` has the identical surface with `await`.
```python theme={"system"}
Caesar(*, api_key=None, base_url=None, timeout=30.0, max_retries=3, http_client=None)
AsyncCaesar(*, api_key=None, base_url=None, timeout=30.0, max_retries=3, http_client=None)
```
| Option | Environment variable | Default | Notes |
| ------------- | -------------------- | --------------------------------- | --------------------------------------------------- |
| `api_key` | `CAESAR_API_KEY` | required | sent as a bearer token |
| `base_url` | `CAESAR_BASE_URL` | `https://alpha.api.trycaesar.com` | trailing slashes are stripped |
| `timeout` | — | `30.0` | per-request timeout in seconds (float) |
| `max_retries` | — | `3` | retries on 429/5xx; `0` disables |
| `http_client` | — | — | bring your own `httpx.Client` / `httpx.AsyncClient` |
Both clients are context managers; outside a `with` block, call `close()` (sync) or `aclose()` (async).
```python theme={"system"}
from caesar_search import AsyncCaesar
async with AsyncCaesar() as client:
results = await client.search("postgres 17 logical replication failover")
```
## Methods
The core methods map to `POST /v1/search`, `POST /v1/document`, `POST /v1/feedback`, plus the [Files knowledge base](/concepts/files) endpoints. Full request and response schemas are in the [API reference](/api-reference/index).
```python theme={"system"}
def search(self, query: str, *, max_results=None, session_id=None,
verbosity=None, max_chars_total=None, extra_body=None) -> SearchResponse
def read(self, target: str | None = None, *, doc_id=None, url=None, query=None,
max_chars=None, start_char=None, include=None,
extra_body=None) -> DocumentResponse
def feedback(self, event_type: str, *, search_id=None, doc_id=None, passage_id=None,
query=None, rank=None, notes=None, extra_body=None) -> FeedbackResponse
# Files knowledge base
def upload_file(self, file: str | PathLike | bytes, *, filename=None,
content_type=None, index=True) -> UploadResult
def presign_upload(self, filename: str, size: int, *, content_type=None) -> FilePresignResponse
def list_files(self) -> FileListResponse
def delete_file(self, name: str) -> FileDeleteResponse
def index_files(self, *, mode="incremental") -> FileIndexResponse
def file_index_status(self, sync_id: str) -> FileIndexStatusResponse
```
### Uploading files
`upload_file()` collapses the presign → upload → index flow into one call. Pass a path (the filename defaults to its basename) or bytes with `filename=`:
```python theme={"system"}
upload = client.upload_file("q3-report.pdf")
status = client.file_index_status(upload.sync_id) # poll until "completed"
hits = client.search(
"q3 revenue",
extra_body={"scope": {"indexes": ["workspace"], "workspace_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 `index_files()` once. See [Files](/concepts/files) for supported types, limits, and the workspace search scope.
### How read() picks doc\_id vs URL
The positional `target` is routed by shape: a UUID-shaped string is sent as `doc_id`; anything else is sent as `canonical_url`. Explicit `doc_id=` or `url=` keywords win when given. With neither, the SDK raises `ValueError("provide a doc_id or a url")`.
Defaults: `include` is `["metadata", "content"]`; content selection is `full_document`; content format is markdown; `max_chars` is omitted unless you pass it. See [documents](/concepts/documents) for the response shape.
### Continuation reads
When `content.truncated` is true, resume from where the previous read stopped:
```python theme={"system"}
from caesar_search import Caesar
client = Caesar()
url = "https://www.postgresql.org/docs/17/logical-replication.html"
doc = client.read(url, max_chars=8000)
text = doc.content.text
if doc.content.truncated:
more = client.read(url, start_char=(doc.content.start_char or 0) + doc.content.char_count)
text += more.content.text
```
A non-zero `start_char` forces `full_document` selection so offsets stay contiguous against the raw document text. Combining `start_char` with `query` will not produce query-relevant selection.
### Response shaping
`search()` exposes the [response shaping](/concepts/response-shaping) controls directly:
```python theme={"system"}
results = client.search("rust async runtime comparison",
verbosity="compact", max_chars_total=4000)
```
`verbosity` is one of `ids_only`, `compact`, `standard` (the default), or `full` — only `full` includes [provenance](/concepts/provenance). On the wire these become `response.verbosity` and `response.budget.max_chars_total`.
## Errors
All six error classes are importable from `caesar_search`. The hierarchy:
| Class | Raised when | Attributes |
| --------------------- | -------------------------------------------------------- | --------------------------------------------------------------- |
| `CaesarError` | base class for everything below | — |
| `APIConnectionError` | the API could not be reached | — |
| `APITimeoutError` | the request timed out (subclass of `APIConnectionError`) | — |
| `APIStatusError` | any non-2xx response | `.status_code`, `.code`, `.message`, `.request_id`, `.response` |
| `AuthenticationError` | HTTP 401 or 403 (subclass of `APIStatusError`) | as `APIStatusError` |
| `RateLimitError` | HTTP 429 (subclass of `APIStatusError`) | as `APIStatusError` |
`.code` is the stable machine-readable code from the [error envelope](/concepts/errors); the exception message is formatted as `code: message`.
```python theme={"system"}
from caesar_search import Caesar, AuthenticationError, RateLimitError, APIStatusError
client = Caesar()
try:
results = client.search("postgres 17 logical replication failover")
except AuthenticationError:
print("check CAESAR_API_KEY") # 401 or 403
except RateLimitError as e:
print("rate limited", e.request_id) # 429, after retries are exhausted
except APIStatusError as e:
print(e.status_code, e.code, e.request_id)
```
### Retries
The client retries statuses 429, 500, 502, 503, and 504 — up to `max_retries` times (default 3, so 4 attempts total) with exponential backoff starting at 0.5 s and capped at 8 s. Caesar rate limits use `X-RateLimit-Reset`; if a numeric `Retry-After` header is present, the client honors it, also capped at 8 s. HTTP-date `Retry-After` values fall back to the exponential schedule. Timeouts and connection failures are never retried — they raise `APITimeoutError` / `APIConnectionError` immediately. After retries are exhausted, the status error for the last response is raised.
## Raw responses and extra\_body
`client.with_raw_response` mirrors all three methods with the same parameters but returns the raw `httpx.Response` (no model validation) — useful for headers like the [rate-limit headers](/rate-limits):
```python theme={"system"}
raw = client.with_raw_response.search("rust async runtime comparison")
print(raw.status_code, raw.headers["X-RateLimit-Remaining"])
```
`extra_body` merges a dict into the request body last, so it can set fields the typed signature does not expose — and it overrides anything the SDK would have set:
```python theme={"system"}
client.search("rust async runtime comparison",
extra_body={"response": {"budget": {"max_chars_total": 4000,
"on_exceed": "error"}}})
```
## Typing
Responses are Pydantic v2 models from `caesar_search.models` (`SearchResponse`, `DocumentResponse`, `FeedbackResponse`, and their nested types). The package ships `py.typed`, so type checkers pick everything up. Field names match the wire format exactly (`search_id`, `doc_id`, `canonical_url`); document metadata lives under `DocumentResponse.doc`, and markdown content lives under `DocumentResponse.content`.
## For agents
* `timeout` is in seconds (`30.0`), not milliseconds. The [TypeScript SDK](/clients/sdks/typescript) uses `timeoutMs` in milliseconds — do not carry values between them unconverted.
* `read()` routes its positional argument purely by UUID shape: UUID goes as `doc_id`, everything else as `canonical_url`. Pass `doc_id=` or `url=` explicitly when ambiguity matters.
* A non-zero `start_char` forces `full_document` selection; pairing it with `query` will not return query-relevant content.
* `extra_body` is merged last and overrides any field the SDK builds, including the `response` wrapper produced by `verbosity`/`max_chars_total`.
* `Caesar()` reads `CAESAR_API_KEY` by default. If no key is provided, API calls fail with `401 missing_api_key`; fail setup early when `CAESAR_API_KEY` is unset.
* Caesar rate limits use `X-RateLimit-Reset`; `Retry-After` is parsed as numeric seconds only when present. HTTP-date values silently fall back to exponential backoff, and timeouts are never retried.
# TypeScript SDK
Source: https://docs.trycaesar.com/clients/sdks/typescript
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
async read(target?: string, options?: ReadOptions): Promise
async feedback(eventType: string, options?: FeedbackOptions): Promise
// Files knowledge base
async uploadFile(options: UploadFileOptions): Promise
async presignUpload(filename: string, size: number, options?: PresignUploadOptions): Promise
async listFiles(): Promise
async deleteFile(name: string): Promise
async indexFiles(options?: IndexFilesOptions): Promise
async fileIndexStatus(syncId: string): Promise
```
```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;
}
interface ReadOptions {
docId?: string;
url?: string;
query?: string;
maxChars?: number;
startChar?: number; // Continue a truncated read from this character offset.
include?: string[];
extraBody?: Record;
}
interface FeedbackOptions {
searchId?: string;
docId?: string;
passageId?: string;
query?: string;
rank?: number;
notes?: string;
extraBody?: Record;
}
```
### 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: "" } },
});
```
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_`; `.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.
# Agent Skills
Source: https://docs.trycaesar.com/clients/skills
Install Caesar's SKILL.md files so your agent learns search, full-document reads, and source citation — served directly by the API, with curl and npx skills options.
Caesar ships [agentskills.io](https://agentskills.io)-format skills that teach an agent to search, read full documents, and cite sources.
For the fastest single-skill install, use the Skills CLI:
```bash theme={"system"}
npx skills add caesar-data/skills --skill caesar-search
```
To install the full API-served bundle, use the Caesar installer:
```bash theme={"system"}
curl -fsSL https://alpha.api.trycaesar.com/skills/install.sh | bash
```
The installer adds the skills into `~/.claude/skills/` and a `caesar-subagent` definition into `~/.claude/agents/` (Claude Code's default locations), then prints: restart your agent session or run `/caesar-setup` to pick them up.
## Two install paths
| Path | Command | Use when |
| ------------- | ---------------------------------------------------------------------- | ------------------------------------------------------------------ |
| Skills CLI | `npx skills add caesar-data/skills --skill caesar-search` | You want the quickest install into an agent that supports skills. |
| API installer | `curl -fsSL https://alpha.api.trycaesar.com/skills/install.sh \| bash` | You want all four skills and the Claude Code subagent in one step. |
Agents that use the `skills` npm CLI can install from the GitHub shorthand instead:
```bash theme={"system"}
npx skills add caesar-data/skills --skill '*' -g -y
```
To install only the runtime search skill:
```bash theme={"system"}
npx skills add caesar-data/skills --skill caesar-search -g -y
```
Use the repository source (`caesar-data/skills`) plus `--skill ...`; bare `npx skills add caesar-search` is not a valid source.
For Claude Code, prefer the curl installer because it also writes `~/.claude/agents/caesar-subagent.md`. The `npx skills` path installs the `SKILL.md` folders only.
## Served by the API itself
The `SKILL.md` files are embedded in the API server and served from `/skills` routes — so the API-hosted installer ships with every API deploy. They are also mirrored to the public [`caesar-data/skills`](https://github.com/caesar-data/skills) repo for `npx skills add`. The `/skills` routes require no authentication.
To update skills later, re-run the same installer. New skill content arrives with API deploys; the installer always fetches the latest version.
## The four skills
| Skill | What it teaches |
| ----------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `caesar-search` | Default for web search, research, lookups, and questions that need information from the web. Results include `doc_id`s you can read as focused excerpts or full documents and cite. |
| `caesar-read` | Read a web page as clean markdown with provenance. Use when you already have a URL or `doc_id`. Handles long pages with character-offset continuation. |
| `caesar-setup` | Install, authenticate, verify, and update the `caesar-search` CLI. Run once, then use the other skills for actual searching. |
| `caesar-best-practices` | Reference for implementing Caesar in code — choosing between the CLI, SDKs, MCP server, and raw HTTP, plus cross-surface conventions. |
## Routes
| Route | Returns |
| --------------------------------------- | ---------------------------------------------------------------- |
| `GET /skills` | JSON index of skills, agents, and the install one-liner |
| `GET /skills/{skill}/SKILL.md` | Skill markdown (the four names above; anything else returns 404) |
| `GET /skills/agents/caesar-subagent.md` | The subagent definition |
| `GET /skills/install.sh` | The installer script |
Browse what's available:
```bash theme={"system"}
curl -s https://alpha.api.trycaesar.com/skills
```
Each entry in the index has `name`, `description`, and `url` fields, so an agent can discover and fetch skills without the installer.
## Installing for other agents
The skill files are standard agentskills.io markdown. Any agent that reads `SKILL.md` files can use them. Use the Skills CLI when available:
```bash theme={"system"}
npx skills add caesar-data/skills --skill caesar-search
```
Or point the API installer at your agent's skills directory:
```bash theme={"system"}
curl -fsSL https://alpha.api.trycaesar.com/skills/install.sh | CAESAR_SKILLS_DIR=~/.agents/skills bash
```
| Variable | Effect |
| ------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `CAESAR_SKILLS_DIR` | Install the skills into this directory instead of `~/.claude/skills`. When set, the `caesar-subagent` file is **not** installed — subagent definitions are a Claude Code concept. |
| `CAESAR_SKILLS_BASE_URL` | Download from a different API base URL (defaults to the host that served the script). |
For non-Claude agents, the `context: fork` and `agent` frontmatter fields are ignored and the skills still work inline.
## How the runtime skills stay context-efficient
`caesar-search` and `caesar-read` declare `context: fork` and `agent: caesar-subagent` in their frontmatter. When invoked, the skill runs in a forked context executed by the `caesar-subagent` — a Bash-only subagent — so raw search and read JSON never lands in the main conversation's token budget. Only the distilled answer comes back.
Because the subagent has no file-read tool, the skills write CLI output with `-o /tmp/$SLUG.json` (which suppresses stdout so harness limits can't truncate it) and the subagent reads the file with `cat`. The subagent's hard rules: only cite URLs that appear in command output, preserve `doc_id` and `search_id` verbatim, and report the output file path so the main context can reference it later without re-searching.
## Relationship to the CLI
The skills are thin drivers for the [`caesar-search` CLI](/clients/cli/usage) — they run `caesar-search search`, `caesar-search read`, and friends rather than calling the API directly. If the CLI is missing, `caesar-setup` walks through [installing it](/clients/cli/install).
The runtime skills also carry a version gate: if the CLI rejects a flag or command as unknown, the installed CLI is outdated — the runtime skills (`caesar-search`, `caesar-read`) instruct the agent to run `caesar-search update` and retry. `caesar-search update` detects its install channel (npm, Homebrew, or standalone) and upgrades in place.
Strict agentskills.io validators flag the Claude Code extension frontmatter fields (`context: fork`, `agent`, `user-invocable`, `argument-hint`) as unknown. This is intentional — consumers ignore unknown fields. Don't "fix" these validation warnings.
## For agents
* Install the search skill with `npx skills add caesar-data/skills --skill caesar-search`, or install the full bundle with the API installer above. Then invoke `/caesar-setup` once where available to verify the CLI and auth. Runtime search and read calls require `CAESAR_API_KEY` or a key stored with `caesar-search auth login`. After that, use `/caesar-search` for queries and `/caesar-read` for URLs or `doc_id`s.
* No installer access? Fetch the skill content directly — `GET /skills` lists every URL, and each `/skills/{skill}/SKILL.md` is plain markdown. These installer routes are public.
* The skill names are a server-side allowlist (`caesar-search`, `caesar-read`, `caesar-setup`, `caesar-best-practices`). Unknown names return a 404 envelope whose `hint` says to `GET /skills`.
* Runtime skills use the CLI and require API auth. Never paste a literal key into chat — use `caesar-search auth login` or the `CAESAR_API_KEY` env var.
* Skills do not auto-update. Re-run the curl installer to refresh them; only the CLI itself self-updates via `caesar-search update`.
# Documents
Source: https://docs.trycaesar.com/concepts/documents
How POST /v1/document works: doc_id and canonical_url lookups, content selection, continuation reads, capture history, and passages.
`POST /v1/document` inspects one canonical document and returns selected content as clean markdown. The usual flow is search → pick a `doc_id` → read, but you can also read any URL directly — unknown URLs are fetched on demand.
```bash cURL theme={"system"}
curl -s https://alpha.api.trycaesar.com/v1/document \
-H "Authorization: Bearer $CAESAR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"canonical_url": "https://www.postgresql.org/docs/17/logical-replication.html",
"query": "how does failover work"
}'
```
```python Python theme={"system"}
from caesar_search import Caesar
client = Caesar()
doc = client.read(
"https://www.postgresql.org/docs/17/logical-replication.html",
query="how does failover work",
)
print(doc.content.text if doc.content else doc.doc.title)
```
```typescript TypeScript theme={"system"}
import { Caesar } from "caesar-search";
const caesar = new Caesar();
// Pass a doc_id or a URL
const doc = await caesar.read("https://www.postgresql.org/docs/17/logical-replication.html", {
query: "how does failover work",
});
console.log(doc.content?.text);
```
```bash CLI theme={"system"}
caesar-search read https://www.postgresql.org/docs/17/logical-replication.html --query "how does failover work" --json
```
## doc\_id or canonical\_url
One of the two lookup keys is required (missing both is a 400 `validation_error`); if you send both, `doc_id` takes precedence and `canonical_url` is ignored:
* `doc_id` — the UUID identifier from a search result. It is deterministic per canonical URL and can be reused across searches and recrawls.
* `canonical_url` — any URL. It is normalized before lookup (tracking params stripped, fragment removed, host lowercased), and unknown URLs are fetched on demand.
For API-key accounts, `doc_id` lookups resolve only documents the account has been granted: the document appeared in one of its search results, or a prior `canonical_url` lookup granted it. An ungranted `doc_id` returns 404 `document_not_found`. A successful `canonical_url` read grants that `doc_id` for future calls — so URL-first, then `doc_id`, always works.
If content cannot be fetched, the response still returns `doc` metadata plus warning `content_unavailable`.
## Choosing sections: include
`include` selects which response sections to compute. Omitted, you get all available sections.
| Value | Returns |
| ----------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `metadata` | the `doc` block (`doc_id`, `canonical_url`, `source_url`, `title`, `meta_description`, `content_digest`, `published_at`, `first_seen_at`, `last_seen_at`, `latest_capture_id`, `headings`) |
| `content` | the `content` block (see below) |
| `passages` | structured `passages` for citation |
| `capture_history` | capture metadata (see below) |
Any other value is a 400 `validation_error`. `include: ["metadata"]` alone skips the content fetch entirely — the cheapest way to check freshness fields. `provenance` is always returned when a capture exists, regardless of `include` — unlike search results, where it is verbosity-gated ([provenance](/concepts/provenance)).
The official SDKs and CLI default to `include: ["metadata", "content"]`; the raw API defaults to all sections. Ask for `passages` or `capture_history` explicitly when using a client.
## Content controls
| Field | Values / default | Notes |
| ------------------------- | ---------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `content.selection` | `none`, `query_relevant`, `top_passages`, `passage_ids`, `full_document` (default) | clients default to `full_document`; use `query_relevant` explicitly when you want excerpts focused by `query`; `none` omits the content block; `full_document` forces a full fetch when only a snippet-derived capture exists |
| `content.format` | `text`, `markdown` (default) | |
| `content.max_chars` | integer, min 1, optional | omit for the full selected content; capped reads that exceed the limit set `content.truncated: true` and warning `content_truncated` — the request never fails for size |
| `content.include_offsets` | boolean | passage `char_start`/`char_end` are included only when true and the capture is a full-document capture |
The response `content` object is `selection`, `format`, `text`, `truncated`, `char_count`, plus `start_char` on range reads.
## Continuation reads
A truncated read is resumable. Do not retry with a bigger cap; continue from `start_char + char_count` using `content.range`.
```bash cURL theme={"system"}
curl -s https://alpha.api.trycaesar.com/v1/document \
-H "Authorization: Bearer $CAESAR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"canonical_url": "https://www.postgresql.org/docs/17/logical-replication.html",
"content": { "selection": "full_document", "range": { "start_char": 12000 } }
}'
```
```python Python theme={"system"}
from caesar_search import Caesar
client = Caesar()
url = "https://www.postgresql.org/docs/17/logical-replication.html"
doc = client.read(url)
while doc.content and doc.content.truncated:
next_offset = (doc.content.start_char or 0) + doc.content.char_count
doc = client.read(url, start_char=next_offset)
```
```typescript TypeScript theme={"system"}
import { Caesar } from "caesar-search";
const caesar = new Caesar();
const url = "https://www.postgresql.org/docs/17/logical-replication.html";
const doc = await caesar.read(url);
if (doc.content?.truncated) {
const next = await caesar.read(url, {
startChar: (doc.content?.start_char ?? 0) + (doc.content?.char_count ?? 0),
});
}
```
```bash CLI theme={"system"}
caesar-search read https://www.postgresql.org/docs/17/logical-replication.html --max-chars 12000 --start-char 12000 --json
```
The `content.range` fields:
| Field | Meaning |
| ------------------ | ------------------------------------------------------------------------- |
| `range.start_char` | character offset to start from (min 0) |
| `range.max_chars` | cap for this range; overrides `content.max_chars` |
| `range.capture_id` | optional capture pin; warns `stale_range` when the latest capture differs |
Rules that make the loop safe:
* The next offset is always `start_char + char_count` from the previous response (`start_char` is omitted when 0). The CLI flag is `--start-char`; the SDKs take `startChar` (TypeScript) / `start_char` (Python).
* Continuation reads must use `selection: "full_document"` — offsets address the raw document text, and selection-based excerpting would make them non-contiguous between calls. The raw API does not change your selection; set it yourself on range reads (as in the cURL example above). The official clients do this automatically whenever a start offset is set.
* A `start_char` past the end of the document returns HTTP 200 with empty `text` and `char_count: 0` — your loop terminates cleanly.
* **`stale_range`**: emitted only when `range.start_char` is greater than 0 and `range.capture_id` is set but no longer matches the latest capture. The read still succeeds and is served from the latest capture, but offsets may not line up with your earlier chunks — restart from `start_char: 0` if exact continuity matters. Details carry `requested_capture_id` and `latest_capture_id`.
## Capture history
`capture_history` entries are `capture_id`, `capture_time`, `content_digest`, and optionally `content_format` (`text` for search-snippet captures, `markdown` for full fetches).
Today `capture_history` returns the latest capture entry only. It is metadata for change detection — compare `content_digest` across reads — not a surface for reading old captures.
Content freshness is managed server-side: a document is re-fetched when no capture exists, the capture has no text, the capture TTL expired (search-derived captures roughly 15 minutes, full-document captures roughly 24 hours), or `full_document` was requested against a snippet-only capture. The `provenance` block (`capture_id`, `capture_time`) always identifies the capture the returned content came from.
## Passages
`passages` are structured spans for citation: `passage_id`, `doc_id`, `ordinal`, `text`, plus optional `section_heading`, `section_path`, and `char_start`/`char_end` (offsets only with `content.include_offsets: true` on a full-document capture).
To re-fetch specific passages from a search result, use `content.selection: "passage_ids"` with `content.passage_ids`. Passage IDs identify passages in the latest capture, so they change when the document content changes. Stale or unknown IDs never fail the request: available passages are returned plus warning `stale_passage_id` listing the missing IDs in `details.passage_ids`. Treat that warning as "the document changed" and re-search or re-read rather than retrying the same IDs.
Full request and response schemas are in the [API reference](/api-reference/index); error envelopes and codes in [errors](/concepts/errors).
# Errors and warnings
Source: https://docs.trycaesar.com/concepts/errors
The error envelope, every error and warning code the API emits, retry guidance, and the CLI exit-code contract.
Errors are JSON with a stable machine-readable code. Trigger one against staging right now:
```bash theme={"system"}
curl -s https://alpha.api.trycaesar.com/v1/search \
-H "Authorization: Bearer $CAESAR_API_KEY" \
-H "Content-Type: application/json" \
-d '{}'
```
```json theme={"system"}
{
"type": "error",
"request_id": "7e9a1f0c-2f43-4f5a-9d3e-6b1c2a4d5e6f",
"error": {
"code": "validation_error",
"message": "query is required",
"details": { "field": "query" }
}
}
```
## The error envelope
Every error response has the same shape:
| Field | Type | Notes |
| --------------- | ------ | ------------------------------------------------------------------------------------------------------------------------------------------- |
| `type` | string | Always the literal `"error"` |
| `request_id` | string | UUID generated per request — quote it when reporting issues |
| `error.code` | string | Stable snake\_case code; branch on this |
| `error.message` | string | Human-readable; wording may change, the code will not |
| `error.details` | object | Optional structured context (often `{"field": "..."}`); omitted when empty |
| `$schema` | string | Optional JSON Schema reference; present on errors returned by the API handlers, absent on middleware-emitted ones (401/403/429). Ignore it. |
There is no `hint` or `retryable` field — retryability is conveyed by HTTP status alone. Error envelopes never include the `access` block; the `X-RateLimit-*` headers are set on every response that reaches the rate limiter, including 400/404/429/5xx errors — but 401/403 authentication errors are rejected before the rate limiter runs and carry no `X-RateLimit-*` headers.
## Error codes
These are the codes the API emits today, exhaustively:
| HTTP | `error.code` | When |
| ---- | ---------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| 400 | `validation_error` | Malformed JSON, an unknown body field, a body over 1 MiB, or an invalid field value. `details.field` names the offender for invalid field values; malformed JSON, unknown fields, and oversized bodies return `details.error` with the parser message instead. Non-UUID `session_id`, `doc_id`, `search_id`, and `passage_id` values land here. |
| 400 | `response_too_large` | Only when you opt in with `response.budget.on_exceed: "error"` and the budget cannot be met. The default (`shed`) degrades with warnings instead — see [response shaping](/concepts/response-shaping). |
| 401 | `missing_api_key` | No Bearer token was sent. |
| 401 | `invalid_api_key` | A Bearer token was sent but is malformed, unknown, expired, or revoked. |
| 403 | `insufficient_scope` | Valid key without the required capability: `search:read`, `document:read`, `feedback:write`, `research:write`, `files:read`, `files:write`, or `usage:read`. |
| 404 | `document_not_found` | `/v1/document` target not found; also `/v1/feedback` when the `doc_id` is not granted to your account. |
| 404 | `search_not_found` | `/v1/feedback` with a `search_id` that does not belong to your account. |
| 429 | `rate_limited` | Over your requests-per-second limit in the active one-second window. See [rate limits](/rate-limits). |
| 500 | `internal_error` | Server fault. Safe to retry. |
| 502 | `provider_unavailable` | The search infrastructure is unreachable. Transient; retry. |
| 503 | `provider_unavailable` | The deployment has no retrieval configured. |
The two 404 codes are ownership-scoped: on `/v1/feedback` they mean the target does not belong to your account, not necessarily that it never existed.
Authentication and permissions are separate: invalid or missing credentials return `401`, while an authenticated key without the required capability returns `403 insufficient_scope`. A valid key can therefore authenticate successfully without being authorized for every endpoint or MCP tool.
## Warnings
Warnings are the API degrading gracefully instead of failing. They share the error shape — `code`, `message`, optional `details` — and arrive in a `warnings` array on search and document responses (`/v1/feedback` has no warnings field):
```json theme={"system"}
{
"code": "content_truncated",
"message": "Returned content was truncated to content.max_chars.",
"details": { "field": "content.text", "max_chars": 12000 }
}
```
**Warnings never invalidate the response.** A 200 with warnings is a complete answer: the status stays 200, the payload is valid and usable, and `warnings` survives every verbosity preset and budget shed.
| `code` | Endpoint | Fires when |
| ------------------------ | -------- | ---------------------------------------------------------------------------------------------------------------------------------------- |
| `response_truncated` | search | `response.budget.max_chars_total` forced shedding; the response's `truncated` flag is set, `details.shed_levels` lists what was dropped. |
| `budget_unsatisfiable` | search | Even a single minimal result exceeds the budget; it is returned anyway. |
| `unknown_field` | search | `response.verbosity` is unrecognized; the request proceeds with `standard`. |
| `source_policy_filtered` | search | The requested `source_policy` filtered out one or more results (`details.filtered_results`). |
| `rerank_unavailable` | search | Reranking is temporarily unavailable; results use first-stage order and `ranking.ranker_version` is `first_stage_order_v1`. |
| `content_unavailable` | document | Document content could not be fetched for this request. |
| `content_truncated` | document | Content exceeded `content.max_chars`; pairs with `content.truncated: true` and `content.start_char` for continuation reads. |
| `stale_range` | document | `content.range.capture_id` pins a capture that is no longer the latest; offsets may not line up. |
| `stale_passage_id` | document | One or more requested passage IDs are unavailable in the latest capture (`details.passage_ids` lists them). |
Missing passage IDs and over-restrictive source policies are deliberately warnings, not errors — your agent gets whatever is available plus a signal about what it did not get.
## Retrying
Retry `429` and `5xx` with backoff; treat `4xx` (other than 429) as bugs in the request, not transient failures.
* There is **no `Retry-After` header**. On 429, read `X-RateLimit-Reset` (RFC3339) — limits use fixed one-second windows, so the reset is at most about a second away. Exponential backoff also works fine.
* Rate-limit tokens are spent before validation, so retry loops sending invalid bodies still burn budget.
* The [SDKs](/clients/sdks/python) and [CLI](/clients/cli/usage) retry automatically: the CLI retries 429 and 500+ up to 3 times with exponential backoff capped at 8 seconds (`--no-retry` disables).
## CLI exit codes
`caesar-search` maps outcomes to exit codes so scripts branch on status, not output parsing. See [CLI automation](/clients/cli/automation).
| Exit code | Meaning |
| --------- | ------------------------------------------------------------------- |
| 0 | Success — including a 200 with warnings |
| 2 | Bad input (usage or flag errors, invalid local input) |
| 3 | Auth error (HTTP 401 or 403) |
| 4 | API error (any other non-2xx, after retries; also network failures) |
| 5 | Timeout (default 30s; raise with `--timeout`) |
With `--json`, CLI errors go to stderr as `{"error":{"code","message","hint"}}` — the `code` is taken from the API envelope's `error.code` when one exists. Exit code 1 is not part of the contract.
# Files
Source: https://docs.trycaesar.com/concepts/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.
```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": ""}},
)
```
```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: "" } },
});
```
```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"
```
## 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": "" }
}
```
* `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.
# How search works
Source: https://docs.trycaesar.com/concepts/how-search-works
How Caesar turns the public web into canonical documents, passages, rankings, and source metadata for agents.
Start with a search that asks for enough context to inspect where each result came from:
```bash theme={"system"}
curl -s https://alpha.api.trycaesar.com/v1/search \
-H "Authorization: Bearer $CAESAR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"query": "postgres 17 logical replication failover",
"max_results": 3,
"response": { "verbosity": "full" }
}'
```
The response is not just a list of URLs. Each result carries a `doc_id`, canonical URL, source URL, timestamps, passages, and capture provenance you can pass to [documents](/concepts/documents), [provenance](/concepts/provenance), and [feedback](/api-reference/index).
## The contract
Caesar is built around canonical documents, not disposable snippets. The search infrastructure discovers public pages, turns them into durable document records, extracts passages, ranks candidates, and returns identifiers that agents can reuse:
| Identifier | Why it matters |
| ------------ | ----------------------------------------------------------------------------------------- |
| `doc_id` | Stable document identity. Use it to read the same canonical document later. |
| `passage_id` | Specific passage identity within a captured document. Use it for citation-grade evidence. |
| `capture_id` | The exact capture that produced the content. Use it to detect drift. |
| `search_id` | The ranked list that produced the result. Use it when sending feedback. |
## Pipeline
The system finds public web documents from crawl queues, links, sitemaps, query demand, recrawl schedules, and source-quality signals. Fetch policy is polite and policy-aware: removals, robots controls, abuse controls, and suppression rules are part of the serving contract.
Multiple URLs can point at the same underlying document. Caesar normalizes them into a canonical record, preserves the observed source URL, and tracks duplicate or alternate paths without making the agent reason about URL clutter.
Fetched content is converted into clean document text, metadata, and passages. Passages keep section context and offsets so an agent can quote the right piece without rereading the whole page.
The serving index uses lexical, structural, freshness, source, and quality signals to retrieve candidates. Query understanding applies normalization, obvious spelling fixes, known-item style queries, and source constraints before ranking.
The API returns ranked results plus the IDs and metadata needed to read, cite, and continue. `compact` protects context windows; `standard` adds passages; `full` adds capture provenance.
## Coverage
Caesar targets the broad public web: technical docs, blogs, forums, PDFs, product pages, news pages, reference material, and long-tail research pages. It is not a vertical-only index and does not expose different schemas for different content categories.
The public docs do not publish a raw page-count number. For agents, raw URL counts are a weak proxy for usefulness; the useful unit is a fresh canonical document with readable content, passage evidence, and provenance. Use result metadata such as `canonical_url`, `source_url`, `last_seen_at`, `last_crawled_at`, `published_at`, `capture_time`, and `content_digest` to judge whether a result is fresh enough for your task.
## Freshness
Freshness is handled at three layers:
| Layer | What happens |
| ----------------- | ---------------------------------------------------------------------------------------------------------------------------------------------- |
| Recrawl policy | Documents with higher change likelihood, demand, or source importance are revisited sooner. |
| Query policy | `freshness_policy` can bias retrieval toward recently published or recently captured material. |
| Response metadata | Search and read responses expose timestamps so agents can decide whether to trust, read again, or search with a stricter freshness constraint. |
When freshness matters, request `response.verbosity: "full"` or read the document after search. That gives the agent capture-level evidence instead of relying on a snippet alone.
## Ranking
Ranking starts with candidate retrieval, then adds second-stage reranking and snippets. If reranking is unavailable, Caesar returns first-stage order with a warning instead of failing the search.
Scores are response-local. Compare scores inside one response only; do not persist them as universal relevance values. The fields to persist are `doc_id`, `passage_id`, `capture_id`, and `search_id`.
## Feedback
Feedback is optional. Send it after a result clearly helped or failed a task:
```bash theme={"system"}
caesar-search feedback \
--event-type result_helpful \
--search-id "$SEARCH_ID" \
--doc-id "$DOC_ID"
```
Feedback is tied to the ranked list via `search_id`, so it lands on the retrieval context that produced the result.
## What Caesar does not hide
Caesar separates retrieval from answer synthesis. `/v1/search` returns ranked evidence, `/v1/document` reads the document, and `/v1/feedback` records what helped. This keeps source identity, freshness, and provenance visible to the agent instead of hiding them behind a generated answer.
# Provenance
Source: https://docs.trycaesar.com/concepts/provenance
Caesar returns source identity and capture metadata so agents can cite exactly what they read, re-read it, and detect when it changes.
Search results can drift: URLs change, snippets age, and the source an agent saw may not match what a human opens later. Caesar returns source identity and capture metadata so a citation can point at content that can be verified later.
```bash cURL theme={"system"}
curl -s https://alpha.api.trycaesar.com/v1/search \
-H "Authorization: Bearer $CAESAR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"query": "linux kernel amd gpu suspend",
"max_results": 1,
"response": { "verbosity": "full" }
}'
```
The result, trimmed and annotated:
```json theme={"system"}
{
"search_id": "a7e2b9c1-3d4f-4a5b-8c6d-9e0f1a2b3c4d", // ID for /v1/feedback
"ranking": {
"ranker_version": "reranked_v1",
"score_scope": "response_local" // scores never compare across responses
},
"results": [
{
"rank": 1, // response-local — never store as a fact
"doc_id": "0c944fa8-4c8f-4f48-9b08-0fb2fd3438ec", // document identity
"canonical_url": "https://example.com/amdgpu-suspend-regression",
"source_url": "https://example.com/amdgpu-suspend-regression?utm_source=feed",
"title": "amdgpu: suspend regression report",
"score": { "value": 0.87 }, // response-local — never store as a fact
"metadata": {
"first_seen_at": "2026-06-12T09:58:00Z",
"last_seen_at": "2026-06-12T09:58:00Z",
"last_crawled_at": "2026-06-12T09:58:00Z",
"extracted_at": "2026-06-12T09:58:00Z",
"content_digest": "sha256:3f6e…", // verify content identity across reads
"published_at": "2026-06-01T00:00:00Z"
},
"provenance": { // only at response.verbosity "full"
"capture_id": "c3d4e5f6-a7b8-4c9d-8e0f-1a2b3c4d5e6f",
"capture_time": "2026-06-12T09:58:00Z"
},
"passages": [
{
"passage_id": "d4e5f6a7-b8c9-4d0e-9f1a-2b3c4d5e6f70",
"doc_id": "0c944fa8-4c8f-4f48-9b08-0fb2fd3438ec",
"ordinal": 1,
"text": "Suspend works on 6.9 but resume fails…"
}
]
}
]
}
```
## The identifiers
All identifiers are plain UUIDs. The content identifiers — `doc_id`, `capture_id`, and `passage_id` — are derived deterministically, so the same input produces the same ID. `search_id` and `request_id` are minted fresh for every response.
| Identifier | Identifies | Derivation and stability |
| ------------ | ------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `doc_id` | The canonical document | Derived from `canonical_url`. The same document keeps the same `doc_id` across searches and recrawls. |
| `capture_id` | One specific capture of the document | Derived from the capture source, a UTC day bucket, and the retrieval request. A re-fetch on a new day is a new capture with a new `capture_id`. |
| `passage_id` | One passage in the latest capture | Derived from `doc_id`, `capture_id`, ordinal, and a hash of the passage text. It changes when the content changes or a newer capture replaces the old one — a latest-capture identifier, not an eternal one. |
| `search_id` | One `/v1/search` response | The ID `/v1/feedback` uses to attribute an event to a ranked result set. |
| `request_id` | One HTTP request | For debugging and support; appears on every response, including errors. |
## Identity vs. response-local
Identity persists across reads; ranking is response-local. Treat them differently:
* **Safe to store and cite**: `doc_id`, `canonical_url`, `capture_id`, `capture_time`, `passage_id` (valid until the content changes), `content_digest`, and `search_id` (for feedback).
* **Never store as facts**: `rank` and `score.value`. The `ranking.score_scope` field is always `"response_local"` — scores compare results within one response only, never across responses or ranker versions. `score` is present only when the second-stage reranker scored the result; when reranking is unavailable and results fall back to first-stage order, it is absent.
Replayed searches may be served from cache. Deterministic ordering, identical scores, and identical `search_id` values across repeated identical queries are not guaranteed. If you need to refer back to a specific result set, keep the `search_id` from the response you actually received.
## canonical\_url vs. source\_url
Both exist because deduplication and citation pull in different directions.
`canonical_url` is the normalized representative URL and the input to `doc_id`: scheme defaults to https, scheme and host are lowercased, the fragment is stripped, tracking parameters (`utm_*`, `fbclid`, `gclid`, `msclkid`) are removed, and the trailing slash is trimmed except at the root. Two URLs that differ only in tracking noise are the same document with the same `doc_id`.
`source_url` is the URL as actually provided or captured — use it when you need the link that was really fetched.
## Timestamps and content\_digest
The `metadata` block on search results carries the document's observation history. All values are RFC3339 strings.
| Field | Meaning |
| ----------------- | ----------------------------------------------------------------------- |
| `first_seen_at` | First time the system saw this canonical document |
| `last_seen_at` | Most recent sighting |
| `last_crawled_at` | Capture time of the underlying capture |
| `extracted_at` | When content was extracted from that capture |
| `published_at` | Best-effort publication date parsed from source metadata; may be absent |
| `content_digest` | `sha256:` followed by the hex digest of the captured content |
`content_digest` is the drift detector: store it alongside anything you cite, and compare it on the next read. A changed digest means the content changed and any claims built on the old capture need re-verification.
## The provenance object
`provenance` is exactly two fields:
```json theme={"system"}
{ "capture_id": "c3d4e5f6-a7b8-4c9d-8e0f-1a2b3c4d5e6f", "capture_time": "2026-06-12T09:58:00Z" }
```
Where it appears differs by endpoint:
* On `/v1/search` results, `provenance` is returned **only** at `response.verbosity: "full"` (see [response shaping](/concepts/response-shaping)). The default `standard` verbosity omits it.
* On `/v1/document`, `provenance` is always present when a capture exists — it names the capture the returned content actually came from.
## Search, read, cite
The IDs connect search results to full-document reads. This runs with `CAESAR_API_KEY`:
```bash cURL theme={"system"}
BASE=https://alpha.api.trycaesar.com
# 1. Search — keep search_id and the doc_id you act on
SEARCH=$(curl -s $BASE/v1/search \
-H "Authorization: Bearer $CAESAR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"query": "linux kernel amd gpu suspend", "max_results": 3}')
SEARCH_ID=$(printf '%s' "$SEARCH" | jq -r .search_id)
DOC_ID=$(printf '%s' "$SEARCH" | jq -r '.results[0].doc_id')
# 2. Read — provenance pins the capture the content came from
DOCUMENT=$(curl -s $BASE/v1/document \
-H "Authorization: Bearer $CAESAR_API_KEY" \
-H "Content-Type: application/json" \
-d "{\"doc_id\": \"$DOC_ID\", \"query\": \"suspend regression\"}")
CAPTURE_ID=$(printf '%s' "$DOCUMENT" | jq -r .provenance.capture_id)
PASSAGE_ID=$(printf '%s' "$DOCUMENT" | jq -r '.passages[0].passage_id')
```
If you send feedback later, use the `search_id`, `doc_id`, and `passage_id` from the response you actually used.
## Why this matters for agents
* **Cite what you actually read.** A bare URL cites whatever the page serves at click time. Citing `doc_id` plus `capture_id` and `capture_time` from the `/v1/document` provenance block pins the claim to the content the agent really consumed.
* **Keep continuation reads honest.** When reading a long document in ranges, pin the capture with `content.range.capture_id`. If a newer capture has replaced it, the response carries a `stale_range` warning instead of silently serving misaligned offsets. See [documents](/concepts/documents) for the continuation loop.
* **Detect content drift.** Compare `content_digest` between reads. Same digest, same content — earlier conclusions still hold. Different digest, re-read before repeating a claim.
* **Tolerate stale passage IDs.** Requesting a `passage_id` that no longer exists in the latest capture does not fail: `/v1/document` returns the passages that are still available plus a `stale_passage_id` warning naming the missing ones.
## Next
* [Search](/concepts/search) — ranking and where source IDs are returned
* [Documents](/concepts/documents) — the read loop, capture freshness, and range reads
* [Quickstart](/quickstart) — search and read in two minutes
# Response shaping
Source: https://docs.trycaesar.com/concepts/response-shaping
Choose how much of each search result the API serializes — verbosity presets and character budgets, decided per call by the caller.
Search results become model input, so response size matters. Caesar lets the caller choose how much each search result serializes: IDs only, compact snippets, standard passages, or full provenance. Every response reports `usage.bytes_returned`, the size of the serialized response.
The same shaped search on every surface:
```bash cURL theme={"system"}
curl -s https://alpha.api.trycaesar.com/v1/search \
-H "Authorization: Bearer $CAESAR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"query": "linux kernel amd gpu suspend",
"max_results": 10,
"response": {
"verbosity": "compact",
"budget": { "max_chars_total": 8000, "on_exceed": "shed" }
}
}'
```
```python Python theme={"system"}
import os
import requests
resp = requests.post(
"https://alpha.api.trycaesar.com/v1/search",
headers={"Authorization": f"Bearer {os.environ['CAESAR_API_KEY']}"},
json={
"query": "linux kernel amd gpu suspend",
"max_results": 10,
"response": {
"verbosity": "compact",
"budget": {"max_chars_total": 8000, "on_exceed": "shed"},
},
},
)
print(resp.json()["usage"]["bytes_returned"], "bytes returned")
```
```typescript TypeScript theme={"system"}
const resp = await fetch(
"https://alpha.api.trycaesar.com/v1/search",
{
method: "POST",
headers: {
"Authorization": `Bearer ${process.env.CAESAR_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
query: "linux kernel amd gpu suspend",
max_results: 10,
response: {
verbosity: "compact",
budget: { max_chars_total: 8000, on_exceed: "shed" },
},
}),
},
);
const data = await resp.json();
console.log(data.usage.bytes_returned, "bytes returned");
```
```bash CLI theme={"system"}
caesar-search search "linux kernel amd gpu suspend" --max-results 10 --format compact --json
```
```json MCP (web_search arguments) theme={"system"}
{
"query": "linux kernel amd gpu suspend",
"max_results": 10,
"response_format": "compact"
}
```
These examples require `CAESAR_API_KEY`. The MCP tool exposes no budget — a fixed 20,000-character server cap is its outer guardrail (see [the remote MCP server](/clients/mcp/remote)).
## Verbosity presets
`response.verbosity` is a string enum — `ids_only`, `compact`, `standard`, `full` — parsed case-insensitively. Presets are cumulative:
| Included | ids\_only | compact | standard | full |
| ---------------------------------------------------------------------------------------------------------------------------------------------- | :-------: | :-----: | :------: | :--: |
| `request_id`, `search_id`, `session_id`; per-result `rank`, `doc_id`, `canonical_url`, `title`; plus `warnings`, `usage`, `truncated` | yes | yes | yes | yes |
| Envelope `access` | — | yes | yes | yes |
| Result `snippet`, `score`; metadata `published_at`, `last_crawled_at` | — | yes | yes | yes |
| Envelope `ranking`; result `source_url`, `description`, `passages`; metadata `first_seen_at`, `last_seen_at`, `extracted_at`, `content_digest` | — | — | yes | yes |
| Result `provenance` (`capture_id`, `capture_time`) | — | — | — | yes |
Extended metadata is part of `standard`, not `full`. The only thing `full` adds over `standard` is the per-result [provenance](/concepts/provenance) block. `standard` matches the pre-shaping default, so a request without a `response` block is unchanged. `score` is an object of the form `{"value": 0.87}` and is present from `compact` up only when a reranking stage scored the result.
Defaults differ by surface, on purpose:
| Surface | Default verbosity | How to change | Budget control |
| -------------------------- | ----------------- | ---------------------------------------------------------------- | ---------------------------- |
| REST `POST /v1/search` | `standard` | `response.verbosity` | `response.budget` |
| MCP `web_search` | `compact` | `response_format` (`compact`, `standard`, `full`; no `ids_only`) | fixed 20,000-char server cap |
| CLI `caesar-search search` | `standard` | `--format` (all four values, maps 1:1 to `response.verbosity`) | none — use `--format` |
`ids_only` is available on REST, the SDKs, and the CLI — not over MCP or the AI SDK tools; it is the cheap probe shape for re-ranking, dedupe, and query-variant evaluation, and pairs well with `/v1/feedback`.
## Budgets and trimming order
`budget.max_chars_total` (minimum 1) caps the serialized response body in characters — roughly 4 characters per token. The guarantee covers the final body you receive, including the truncation warning and the `usage` block. `budget.on_exceed` is `shed` (default) or `error`; `shed` means the server trims lower-priority fields until the response fits. With `error`, no trimming occurs and the request fails with HTTP 400, code `response_too_large`. Verbosity projection is applied first, then the budget is enforced.
When the budget binds under `shed`, payload is removed in this exact order, re-measuring after each step:
1. `passages` — lowest-ranked result first, last passage first, one at a time
2. `snippets` — snippets over 200 characters trimmed to 200 plus an ellipsis
3. `provenance` — dropped across all results
4. `extended_metadata` — metadata trimmed back to `published_at` and `last_crawled_at`
5. `description` — dropped across all results
6. `tail_results` — trailing results dropped, never below one result
Any trimming sets `"truncated": true` on the envelope and appends one `response_truncated` warning whose message is the format `Budget N chars: shed X, Y.` — the names above are the exact strings in `details.shed_levels`:
```json theme={"system"}
{
"truncated": true,
"warnings": [
{
"code": "response_truncated",
"message": "Budget 8000 chars: shed passages, tail_results.",
"details": {
"max_chars_total": 8000,
"shed_levels": ["passages", "tail_results"],
"results_returned": 8,
"results_ranked": 10
}
}
],
"usage": { "requests": 1, "bytes_returned": 7912 }
}
```
If even a single result with invariant fields exceeds the budget, it is returned anyway with a `budget_unsatisfiable` warning ("A single result with invariant fields exceeds the N character budget; returning it anyway."). You never get an empty 200, and a response is never an error just for being big.
Account grants are recorded over the full ranked set before shedding. A result shed at `tail_results` can still be fetched later by `doc_id` via `/v1/document`, and `/v1/feedback` on its rank remains valid — `results_ranked` in the warning tells you how many were ranked.
## What always survives
Identifiers are invariant at every verbosity-and-budget combination: `request_id`, `search_id`, `session_id`, and per result `rank`, `doc_id`, `canonical_url`, `title`, plus `warnings`, `usage`, and the `truncated` flag. The leanest possible response still carries enough fields to fetch everything else later via [`/v1/document`](/concepts/documents).
## What each shape costs
Estimates for an 8-result search (results typically carry 2–4 passages each; only the MCP surface caps passages at 2 per result):
| Shape | Per result | 8 results | Approx tokens (chars/4) |
| --------------------------------------------------- | ------------: | -------------: | ----------------------: |
| Passages shape (snippet + passages + dates + score) | \~3,500 chars | \~28,000 chars | \~7,000 |
| Compact (snippet, no passages) | \~700 chars | \~5,900 chars | \~1,500 |
| IDs only (rank, doc\_id, url, title) | \~270 chars | \~2,200 chars | \~540 |
A 4–13x spread per call, multiplied by agent-loop length. Only the caller knows which row a step needs: a "find the right document" step wants IDs plus snippets and follows with a read; a "quote evidence" step wants the passages.
## Invalid values by surface
On REST, an unrecognized `response.verbosity` does not fail the request: the server uses `standard` and appends an `unknown_field` warning ("response.verbosity value is not recognized; using standard.", with `details.field` and `details.value`). At MCP, `ids_only` and any unknown `response_format` silently coerce to `compact` with no warning. The CLI rejects invalid `--format` values locally with exit code 2.
## Scope
Shaping via the `response` block applies to `/v1/search` only. `POST /v1/document` takes no `response` block — document payloads are shaped by `include` sections, `content.max_chars`, and `content.range` continuation reads instead (see [Documents](/concepts/documents)).
Full request schemas live in the [API reference](/api-reference/index).
# Search
Source: https://docs.trycaesar.com/concepts/search
How POST /v1/search works: query variants, result limits, filters, freshness and source policies, and the ranking contract.
`POST /v1/search` runs ranked retrieval over canonical web documents. Every result includes a `doc_id` and `canonical_url` you can pass straight to [/v1/document](/concepts/documents), plus [provenance](/concepts/provenance) fields you can cite when you need auditability.
```bash cURL theme={"system"}
curl -s https://alpha.api.trycaesar.com/v1/search \
-H "Authorization: Bearer $CAESAR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"query": "postgres 17 logical replication failover",
"max_results": 5
}'
```
```python Python theme={"system"}
from caesar_search import Caesar
client = Caesar() # reads CAESAR_API_KEY
results = client.search(
"postgres 17 logical replication failover",
max_results=5,
)
for r in results.results or []:
print(r.rank, r.title, r.canonical_url)
```
```typescript TypeScript 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);
}
```
```bash CLI theme={"system"}
caesar-search search "postgres 17 logical replication failover" --max-results 5 --json
```
These examples use `CAESAR_API_KEY`; see [authentication](/authentication) for key setup.
Reranking is a modular second stage and never fails a request: if it is unavailable, results fall back to first-stage order with warning `rerank_unavailable` and `ranking.ranker_version` reports `first_stage_order_v1`.
## Query and query variants
`query` is the only required field — the original user or agent question. `search_queries` optionally supplies your own rewrites as an array of strings: the first entry, when non-empty, replaces `query` as the candidate first-stage query, and the full list is given as context to the server-side query transform. If your agent already produced good rewrites, send them — you skip the server-side rewrite step.
## max\_results
`max_results` is an integer from 1 to 50, default 10. It is the complete result window — there is no cursor or offset pagination. The server fills the window itself by paging first-stage retrieval internally and deduplicating URLs, so asking for 50 returns the largest available result window; you never page manually.
You may still receive fewer than `max_results` when sources run dry. That is not an error and does not set the envelope `truncated` flag — `truncated` refers only to [response-shaping budget sheds](/concepts/response-shaping).
## Filters
| Field | Type | Effect |
| --------------------- | --------------------- | ------------------------------------------------------------------------------------- |
| `filters.country` | string (single value) | forwarded to first-stage retrieval |
| `filters.language` | string (single value) | forwarded to first-stage retrieval |
| `filters.exact_match` | boolean | wraps the candidate query in double quotes (exact phrase) unless it is already quoted |
## Freshness policy
| Field | Accepts | Behavior |
| ---------------------------------- | --------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `freshness_policy.published_after` | RFC3339 timestamp or `YYYY-MM-DD` | mapped to a coarse recency window — past day, past week, past month, past year, or an explicit `YYYY-MM-DDtoYYYY-MM-DD` range for older dates. Unparseable values are ignored. |
| `freshness_policy.freshness` | raw window token: `pd`, `pw`, `pm`, `py`, or `YYYY-MM-DDtoYYYY-MM-DD` | passed through to the search infrastructure; used only when `published_after` is absent |
## Source policy
| Field | Type | Effect |
| ------------------------------------ | --------- | ------------------------------------------------------------------------------------------------------------------------------------- |
| `source_policy.include_domains` | string\[] | with exactly one domain, the first-stage query is scoped with a `site:` operator; all entries also act as a post-retrieval allow list |
| `source_policy.exclude_domains` | string\[] | post-retrieval deny list |
| `source_policy.require_domain_match` | boolean | when true and `include_domains` is non-empty, results not matching the allow list are dropped |
Domain matching is host-suffix based: `example.com` matches `example.com` and any subdomain; a leading `www.` is ignored. When the policy removes results, the response carries warning `source_policy_filtered` with `details.filtered_results` counting what was dropped — so a thin result set is explainable rather than silent.
## The ranking block
Successful responses include a `ranking` object describing how results were ordered:
| Field | Values |
| ---------------- | ---------------------------------------------------------------------------------------------------------------- |
| `ranker_version` | `first_stage_order_v1` (first-stage order, no rerank) or `reranked_v1` (second-stage reranker reordered results) |
| `score_scope` | always `response_local` |
Scores are response-local. A result `score` (an object like `{"value": 0.87}`, present only when the rerank stage scored that result) is comparable to other scores in the same response only — never across responses or ranker versions. Do not persist scores as absolute relevance.
Replayed searches may be served from cache; deterministic ordering, identical scores, and identical `search_id` values are not guaranteed across calls.
## Analytics request field
`client_model` is accepted for analytics and tuning, but is **not forwarded into retrieval**. Sending it is harmless; expecting retrieval behavior from it is a bug.
## A full request
Every implemented request field in one call (the `response` block is covered in [response shaping](/concepts/response-shaping)):
```bash cURL theme={"system"}
curl -s https://alpha.api.trycaesar.com/v1/search \
-H "Authorization: Bearer $CAESAR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"query": "postgres 17 logical replication failover",
"search_queries": ["postgres 17 failover slot synchronization"],
"max_results": 20,
"filters": { "country": "us", "language": "en", "exact_match": false },
"freshness_policy": { "published_after": "2024-09-01" },
"source_policy": {
"include_domains": ["postgresql.org"],
"require_domain_match": true
},
"response": { "verbosity": "standard" }
}'
```
## The response
Trimmed to one result, at the default `standard` verbosity:
```json theme={"system"}
{
"request_id": "5f0c2c4e-1d2a-4f6b-9e0a-7c1b2d3e4f50",
"search_id": "a7e2b9c1-3d4f-4a5b-8c6d-9e0f1a2b3c4d",
"session_id": "b1c2d3e4-f5a6-4b7c-8d9e-0f1a2b3c4d5e",
"access": { "rate_limit": { "limit_rps": 100, "remaining": 99, "reset_at": "2026-06-12T10:00:01Z" } },
"ranking": { "ranker_version": "reranked_v1", "score_scope": "response_local" },
"results": [
{
"rank": 1,
"doc_id": "0c944fa8-4c8f-4f48-9b08-0fb2fd3438ec",
"canonical_url": "https://www.postgresql.org/docs/17/logical-replication-failover.html",
"source_url": "https://www.postgresql.org/docs/17/logical-replication-failover.html",
"title": "Logical Replication Failover",
"description": "How to ensure logical replication continues after failover…",
"snippet": "When the publisher server fails, the subscriber can be redirected…",
"score": { "value": 0.87 },
"metadata": {
"first_seen_at": "2026-06-12T09:58:00Z",
"last_seen_at": "2026-06-12T09:58:00Z",
"last_crawled_at": "2026-06-12T09:58:00Z",
"extracted_at": "2026-06-12T09:58:00Z",
"content_digest": "sha256:3f6e…",
"published_at": "2024-09-26T00:00:00Z"
},
"passages": [
{ "passage_id": "d4e5f6a7-b8c9-4d0e-9f1a-2b3c4d5e6f70", "doc_id": "0c944fa8-4c8f-4f48-9b08-0fb2fd3438ec", "ordinal": 1, "text": "When the publisher server fails…" }
]
}
],
"usage": { "requests": 1, "bytes_returned": 2148 }
}
```
Notes on the envelope:
* `search_id` identifies the ranked list and can be used with `/v1/feedback`. `session_id` echoes yours or is server-generated — see [sessions](/concepts/sessions).
* Per-result `provenance` (`capture_id`, `capture_time`) appears only at `response.verbosity: "full"` — the default `standard` omits it. See [response shaping](/concepts/response-shaping).
* Error responses use a shared envelope with stable machine codes — see [errors](/concepts/errors). Full field schemas live in the [API reference](/api-reference/index).
# Sessions
Source: https://docs.trycaesar.com/concepts/sessions
Group a multi-step task with a session_id so feedback and ranking signals connect across calls.
Every response includes a `session_id`. If you do not send one, the server generates a fresh UUID for that request and returns it:
```bash theme={"system"}
curl -s https://alpha.api.trycaesar.com/v1/search \
-H "Authorization: Bearer $CAESAR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"query":"linux kernel scheduler","max_results":2}'
```
```json theme={"system"}
{
"request_id": "cdef84e8-d7ed-4e53-9d1c-243b2dbd4b57",
"search_id": "3de031fb-c7e0-4ef3-ad56-ac6ccbbc78c6",
"session_id": "9cb3c9a5-c15b-4763-8fec-40565faf54db",
"results": ["..."]
}
```
Pass that value back on subsequent calls in the same task — follow-up searches, document reads, feedback — and the server can connect them:
```bash theme={"system"}
curl -s https://alpha.api.trycaesar.com/v1/search \
-H "Authorization: Bearer $CAESAR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"query": "EEVDF scheduler latency",
"session_id": "9cb3c9a5-c15b-4763-8fec-40565faf54db"
}'
```
## How to send it
| Endpoint | Body field `session_id` | Header `X-Session-ID` |
| ------------------- | ----------------------- | --------------------- |
| `POST /v1/search` | yes | yes |
| `POST /v1/document` | no — header only | yes |
| `POST /v1/feedback` | yes | yes |
**The body wins.** If both a body `session_id` and an `X-Session-ID` header are present, the body value is used. Set the header once in your HTTP client as a convenient default; override per request in the body when you need to.
```bash theme={"system"}
curl -s https://alpha.api.trycaesar.com/v1/document \
-H "Authorization: Bearer $CAESAR_API_KEY" \
-H "Content-Type: application/json" \
-H "X-Session-ID: 9cb3c9a5-c15b-4763-8fec-40565faf54db" \
-d '{"canonical_url": "https://documentation.ubuntu.com/real-time/latest/explanation/schedulers"}'
```
The value must be a UUID. Anything else is rejected with `400 validation_error` — message `session_id must be a UUID` for the body field, `X-Session-ID must be a UUID` for the header.
## What sessions are — and are not
Sessions provide continuity for ranking, telemetry, and abuse controls, and they give [feedback](/api-reference/index) its task context: a `search` followed by a `document` read followed by `feedback` under one `session_id` reads as a single multi-step task.
What a session is **not**:
* **Not server-side state.** Sessions are not persisted as entities, and a supplied `session_id` is never checked for "existence" — continuity is entirely caller-driven. Any valid UUID works.
* **Not authentication.** A `session_id` grants nothing. Auth is the Bearer key — see [Authentication](/authentication).
* **Not a rate-limit key.** [Rate limits](/rate-limits) are keyed by API key; the session has no effect.
## `client_model`
`client_model` is an optional string identifying the calling model (for example `my-agent-model`), used for analytics and tuning. It appears in two places:
| Where | Field | Status |
| ------------------------ | ---------------------------- | ------------------------------------------------------ |
| `POST /v1/search` body | `client_model` (top level) | Accepted; reserved for analytics — not persisted today |
| `POST /v1/feedback` body | `agent_context.client_model` | Persisted with the feedback event |
If you want model attribution to count, set it on feedback via `agent_context`. The hosted [MCP server](/clients/mcp/remote) sets `client_model: "mcp"` on searches it makes on your behalf. There is no header form of `client_model`.
## `X-Caesar-Client`
First-party clients send an `X-Caesar-Client: client/version` header: `cli/0.2.0`, `python-sdk/0.2.0`, `ts-sdk/0.2.0`. It is an attribution convention, not a protocol — the server does not parse, validate, or store it today, and it changes no behavior. If you build an integration, sending `yourtool/1.2.3` is encouraged:
```bash theme={"system"}
curl -s https://alpha.api.trycaesar.com/v1/search \
-H "Authorization: Bearer $CAESAR_API_KEY" \
-H "Content-Type: application/json" \
-H "X-Caesar-Client: yourtool/1.2.3" \
-d '{"query":"site reliability postmortem patterns"}'
```
Unlike `session_id`, there is no body equivalent and no precedence rule to think about.
# Caesar
Source: https://docs.trycaesar.com/index
Caesar is the search engine for AI, organising the world's data with an API for web search and full-document reads.
Caesar is the search engine for AI, organising the world's data. Use the API to search the web, read sources in full, and cite the sources used. [Sign up in the Caesar app](https://app.trycaesar.com) to create an API key. For a limited time, all accounts receive a \$1,000 starting credit grant.
## Start with an agent
For agents that support skills, install Caesar search directly:
```bash theme={"system"}
npx skills add caesar-data/skills --skill caesar-search
```
For agents: use `/llms.txt` to discover docs, append `.md` to any page for markdown, and start with `/agents/install` for client-specific setup.
## Call the API directly
This works as written with `curl` and `CAESAR_API_KEY`:
```bash theme={"system"}
curl -s https://alpha.api.trycaesar.com/v1/search \
-H "Authorization: Bearer $CAESAR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"query": "postgres 17 logical replication failover", "max_results": 3}'
```
Each result includes a `doc_id` you can pass to `/v1/document` to read the page in full. [Run the first search and read](/quickstart).
## Search, read, cite
Get ranked documents from the web.
Read the selected `doc_id` as markdown.
Cite the returned source URL and capture metadata.
## Search and read everywhere
| Verb | Endpoint | What it does |
| ------ | ------------------- | ------------------------------------------------------------------------------------------------------------ |
| search | `POST /v1/search` | Ranked retrieval over canonical documents and passages |
| read | `POST /v1/document` | Inspect one document and retrieve its content (full-page markdown with `content.selection: "full_document"`) |
The same search and read operations appear on every surface: `search`/`read` in the CLI and SDKs, `web_search`/`web_fetch` over MCP. Feedback is available from REST, the CLI, and SDKs when you need it.
## Citable sources
Search results can drift: URLs change, snippets age, and the source an agent saw may not match what a human opens later. Caesar returns source metadata agents can keep with their answer:
* **`doc_id`** — deterministic identifier for a canonical document that can be reused across searches and recrawls. Read it in full any time with `/v1/document`.
* **`passage_id`** — identifier for a specific passage in the latest capture, with section context.
* **`search_id`** — identifier for the search itself, used to attribute feedback to the exact ranking that produced a result.
* **`capture_id` and `capture_time`** — exactly which capture of the document your content came from (on search results with `response.verbosity: "full"`; always available from `/v1/document`).
That means an agent can search, read the selected source, and cite where and when the content was captured. See [provenance](/concepts/provenance) for the full object model.
## Pick a surface
Three endpoints, an OpenAPI spec, and an interactive playground. Start here for raw HTTP.
Canonical documents, passages, freshness, ranking, and source metadata.
API usage is credit-backed. New accounts get a limited-time \$1,000 starting credit grant.
`pip install caesar-search` — sync and async clients, typed models, automatic retries.
`npm install caesar-search` — fully typed, ESM and CJS, works in Node, Bun, Deno, and edge runtimes.
`web_search` and `web_fetch` from the `caesar-search/ai` subpath.
`caesar-search` — search and read from the shell, with a `--json` contract built for scripts and agents.
Remote MCP endpoint at `/mcp` — generic web-search tools, no local install, one command to connect.
Recipes for framework tools, generic tool calling, and Agent Development Kit projects.
Install the search skill with `npx skills add`, or use the API installer for the full bundle.
## Next steps
* [Quickstart](/quickstart) — sign up, set `CAESAR_API_KEY`, then search and read with curl.
* [How search works](/concepts/how-search-works) - how Caesar turns public pages into citable documents and passages.
* [Pricing](/pricing) - API usage, credits, and the limited-time starting grant.
* [Authentication](/authentication) — creating, storing, and sending API keys.
* [Agents guide](/agents/agents-guide) — conventions for building agents on Caesar.
# Pricing
Source: https://docs.trycaesar.com/pricing
Caesar search uses account credits for API usage, with a limited-time $1,000 starting credit grant for new accounts.
Caesar API usage is credit-backed. [Sign up in the Caesar app](https://app.trycaesar.com), create an API key, and add credits when you need more capacity. For a limited time, all accounts receive a \$1,000 starting credit grant.
```bash theme={"system"}
curl -s https://alpha.api.trycaesar.com/v1/search \
-H "Authorization: Bearer $CAESAR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"query": "postgres 17 logical replication failover", "max_results": 3}'
```
## Getting started
| Step | Action |
| -------------- | ---------------------------------------------------------------------------------------- |
| Create account | [Open the Caesar app](https://app.trycaesar.com) and sign up. |
| Get a key | Create an API key and store it in `CAESAR_API_KEY` or your secret manager. |
| Start building | Use the \$1,000 limited-time starting credit grant for search, read, and feedback calls. |
| Add capacity | Top up credits in the app when your balance runs low. |
All API calls use the same public endpoints and response shapes. A key identifies the account that owns usage, rate limits, document grants, and feedback attribution. See [authentication](/authentication).
## Rate limits
| Credential | Included limit |
| ---------- | -----------------------------------------: |
| API key | Per-key limit, default 100 requests/second |
Every response includes `X-RateLimit-*` headers and an `access.rate_limit` block so agents can back off without guessing. Details: [rate limits](/rate-limits).
## Credits
Credits fund API usage across search, read, and feedback. The app shows balance, usage, payment methods, and top-ups for the account or team that owns the key.
For a limited time, all accounts receive a \$1,000 starting credit grant. The grant is meant to cover evaluation and early integration work before you add paid credits.
## Getting more capacity
Create additional API keys, manage credits, and add payment methods in the [Caesar app](https://app.trycaesar.com). Keep keys in `CAESAR_API_KEY`, the CLI key store, or your secret manager; never paste key material into a chat or docs page.
## FAQ
Yes. Search, read, and feedback calls require an API key. Create one in the [Caesar app](https://app.trycaesar.com).
A key attaches requests to your account for credits, rate limits, document grants, and feedback attribution. It does not change endpoint URLs, response fields, or the search-read-feedback contract.
For a limited time, all accounts get a \$1,000 credit grant for API usage while they evaluate and build their first integration.
The API returns HTTP 429 with `X-RateLimit-*` headers. Wait until `X-RateLimit-Reset`, reduce fan-out, or contact Caesar if your integration needs a higher per-key limit.
Use the app to manage keys and billing, then contact Caesar if you need a custom per-key limit.
# Connect Caesar to your agent
Source: https://docs.trycaesar.com/quickstart
Sign up for Caesar, install search into an agent, verify search and read, then see the raw API loop.
Sign up in the [Caesar app](https://app.trycaesar.com), create an API key, and set it as `CAESAR_API_KEY`. For a limited time, all accounts receive a \$1,000 starting credit grant. Choose the path that matches where your agent runs, verify one search, then ask the agent to search and read.
For agents: use `/llms.txt` to discover docs, append `.md` to any page for markdown, and start with `/agents/install` when you need client-specific setup.
## Choose an install path
```bash theme={"system"}
export CAESAR_API_KEY="..."
```
```bash theme={"system"}
npx skills add caesar-data/skills --skill caesar-search
```
Best for coding agents that read `SKILL.md`. The skill teaches the agent to search, read, preserve source URLs and IDs, and use the CLI when available.
```bash theme={"system"}
caesar-search search "hello world" \
--max-results 1 \
--format compact \
--json
```
Success means exit code 0 and a JSON envelope containing `search_id`.
```bash theme={"system"}
claude mcp add --transport http caesar "https://alpha.api.trycaesar.com/mcp?profile=web-search" \
--header "Authorization: Bearer $CAESAR_API_KEY"
```
Best for chat and IDE clients that support remote tools. The `web-search` profile exposes `web_search` and `web_fetch`, so agents naturally choose Caesar for ordinary web lookups. Pass `Authorization: Bearer $CAESAR_API_KEY` in the MCP config.
```bash theme={"system"}
curl -s "https://alpha.api.trycaesar.com/mcp?profile=web-search" \
-H "Authorization: Bearer $CAESAR_API_KEY" \
-H 'Content-Type: application/json' \
-H 'Accept: application/json, text/event-stream' \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}'
```
Success means the tool list contains `web_search` and `web_fetch`.
```bash theme={"system"}
npm install -g caesar-search-cli
```
Best for terminal agents, scripts, and CI jobs. The CLI keeps output machine-readable with `--json`.
```bash theme={"system"}
caesar-search search "hello world" \
--max-results 1 \
--format compact \
--json
```
Success means the response includes `search_id` and at least one result.
```bash theme={"system"}
npm install caesar-search ai
```
Best when you are building an app and want Caesar as model tools.
```ts theme={"system"}
import { generateText } from "ai";
import { caesarTools } from "caesar-search/ai";
const { text } = await generateText({
model,
tools: caesarTools(),
prompt: "Search for Postgres 17 logical replication changes, then read the best result.",
});
```
The tools are `web_search` and `web_fetch`; the default client reads `CAESAR_API_KEY`.
```bash theme={"system"}
node -e 'import("caesar-search/ai").then(m => console.log(typeof m.caesarTools))'
```
Success prints `function`.
```bash theme={"system"}
curl -s https://alpha.api.trycaesar.com/v1/search \
-H "Authorization: Bearer $CAESAR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"query": "postgres 17 logical replication failover", "max_results": 3}'
```
Best when you are integrating Caesar directly from a backend, worker, or test harness.
```bash theme={"system"}
curl -s https://alpha.api.trycaesar.com/v1/search \
-H "Authorization: Bearer $CAESAR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"query": "hello world", "max_results": 1}' | jq '{search_id, result_count: (.results | length)}'
```
Success means `search_id` is present and `result_count` is at least 1.
Use the raw API calls below to read the top result. Feedback is optional.
## Search, read, cite
Query returns ranked results from the web.
Pass `doc_id` to read clean markdown plus capture metadata.
Preserve `source_url`, `passage_id`, `capture_id`, and `capture_time`.
## Not sure which path?
| Use this path | When |
| ------------- | ----------------------------------------------------------------- |
| Skill | Your agent supports Skills and can read `SKILL.md`. |
| MCP | Your chat or IDE client supports remote MCP tools. |
| CLI | Your agent has shell access or you are scripting from a terminal. |
| AI SDK | You are building an app with model tools. |
| REST | You want the underlying HTTP contract directly. |
## Raw API calls
The main search and read path is two HTTP calls. A third feedback call is optional when you want to record which result helped. This works with `curl`, `jq`, and `CAESAR_API_KEY`.
```bash theme={"system"}
RESPONSE=$(curl -s https://alpha.api.trycaesar.com/v1/search \
-H "Authorization: Bearer $CAESAR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"query": "postgres 17 logical replication failover", "max_results": 3}')
printf '%s' "$RESPONSE" | jq '{search_id, results: [.results[] | {rank, doc_id, title}]}'
```
Save the IDs the next steps need:
```bash theme={"system"}
SEARCH_ID=$(printf '%s' "$RESPONSE" | jq -r '.search_id')
DOC_ID=$(printf '%s' "$RESPONSE" | jq -r '.results[0].doc_id')
```
```bash theme={"system"}
curl -s https://alpha.api.trycaesar.com/v1/document \
-H "Authorization: Bearer $CAESAR_API_KEY" \
-H "Content-Type: application/json" \
-d "{\"doc_id\": \"$DOC_ID\", \"content\": {\"selection\": \"full_document\", \"max_chars\": 2000}}" \
| jq -r '.content.text'
```
If `content.truncated` is true, continue from the previous response's `content.start_char` plus `content.char_count`. See [documents](/concepts/documents).
```bash theme={"system"}
curl -s https://alpha.api.trycaesar.com/v1/feedback \
-H "Authorization: Bearer $CAESAR_API_KEY" \
-H "Content-Type: application/json" \
-d "{\"event_type\": \"result_helpful\", \"search_id\": \"$SEARCH_ID\", \"doc_id\": \"$DOC_ID\", \"rank\": 1}" \
| jq '{feedback_id, accepted}'
```
Search and read are the main path. Feedback is available when your app or agent can tell which result was useful.
## Build with Caesar
`pip install caesar-search`
`npm install caesar-search`
`caesarTools()` exposes `web_search` and `web_fetch`.
Wrap Caesar for framework tools, generic tool calling, and Agent Development Kit projects.
## Where next
* [Install for agents](/agents/install) - per-client install blocks.
* [How search works](/concepts/how-search-works) - coverage, freshness, ranking, and provenance.
* [Search](/concepts/search) - filters and ranking behavior.
* [Documents](/concepts/documents) - content selection, truncation, and continuation reads.
* [Pricing](/pricing), [authentication](/authentication), and [rate limits](/rate-limits) - credits, API keys, and request ceilings.
# Rate limits
Source: https://docs.trycaesar.com/rate-limits
Per-second API-key rate limits, the X-RateLimit response headers, and how to back off on 429s.
Every response tells you exactly where you stand. Check your limit with one request:
```bash theme={"system"}
curl -s -o /dev/null -D - \
https://alpha.api.trycaesar.com/v1/search \
-H "Authorization: Bearer $CAESAR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"query": "hello", "max_results": 1}' | grep -i x-ratelimit
```
```text theme={"system"}
x-ratelimit-limit-rps: 100
x-ratelimit-remaining: 99
x-ratelimit-reset: 2030-01-01T00:00:01Z
```
## Limits
| Credential | Limit | Counted per |
| ---------------------- | --------------------------- | ---------------------- |
| API key or OAuth token | default 100 requests/second | authenticated identity |
API keys can carry custom per-key limits. See [authentication](/authentication).
## How counting works
* Limits are enforced in **fixed 1-second windows**. The counter is shared across server instances, so the limit holds globally, not per instance.
* **Every request that passes authentication consumes one token, regardless of outcome.** The rate-limit decision happens before request validation, so a `400 validation_error` still costs a token — hammering with malformed requests will hit 429. Requests rejected with `401` or `403` during authentication are refused before the limiter and do not consume a token.
* On the [MCP server](/clients/mcp/remote), **each JSON-RPC message counts as one request**.
## Response headers
The three `X-RateLimit-*` headers are set on every response that reaches the rate limiter — success and error alike, including 400s and 429s. The only exceptions are requests rejected during authentication (`401`/`403`), which are refused before rate limiting runs and carry no `X-RateLimit-*` headers.
| Header | Meaning |
| ----------------------- | ---------------------------------------------- |
| `X-RateLimit-Limit-RPS` | your per-second limit |
| `X-RateLimit-Remaining` | requests left in the active window |
| `X-RateLimit-Reset` | RFC 3339 timestamp when the next window starts |
There is no `Retry-After` header. On a 429, wait until `X-RateLimit-Reset` — at most about one second away — or use exponential backoff. The [SDKs](/clients/sdks/python) and [CLI](/clients/cli/usage) already do this: they retry 429s (and 5xxs) automatically with exponential backoff, so you only need to handle 429 yourself when calling the raw API.
For direct HTTP agents, keep retries narrow:
```text theme={"system"}
if response.status == 429:
wait_until(response.headers["X-RateLimit-Reset"])
retry_same_request_once()
else:
handle_response()
```
Do not fan out retries after a 429. Retry the same request once after the reset window, then continue with fewer concurrent calls.
## The access block in response bodies
Every success envelope mirrors the headers in an `access` block:
```json theme={"system"}
"access": {
"rate_limit": {
"limit_rps": 100,
"remaining": 99,
"reset_at": "2030-01-01T00:00:01Z"
}
}
```
Two caveats: search responses at `response.verbosity: "ids_only"` shed the `access` block to save tokens, and error envelopes never include it. The `X-RateLimit-*` headers are always present on responses that reach the rate limiter and are the source of truth.
## The 429 response
When you exceed your limit, the request fails with status `429`, `X-RateLimit-Remaining: 0`, and the standard error envelope:
```json theme={"system"}
{
"type": "error",
"request_id": "48a7a262-156a-4a96-9b05-2071ccd7374a",
"error": { "code": "rate_limited", "message": "rate limit exceeded" }
}
```
See [errors](/concepts/errors) for the full envelope contract.
## Abuse controls
Caesar's limits are deliberately permissive: abuse is handled by detection and response — throttling or blocking abusive traffic patterns — rather than preventive caps on everyone else.