POST /v1/search runs ranked retrieval over canonical web documents. Every result carries stable handles (doc_id, canonical_url) you can pass straight to /v1/document, plus provenance you can cite.
curl -s https://search-api-staging-779189860552.europe-west1.run.app/v1/search \
-H "Content-Type: application/json" \
-d '{
"query": "postgres 17 logical replication failover",
"mode": "standard",
"max_results": 5
}'
Anonymous access works as written. Send Authorization: Bearer $CAESAR_API_KEY only if you have a partner key for higher throughput — see authentication.
Modes
mode sets retrieval depth. Default is standard. Any other value returns 400 unsupported_mode.
| fast | standard (default) | research |
|---|
| Second-stage reranking | no (first-stage order) | yes | yes |
| Extra per-result snippets | no | yes | yes |
| Additional context-enrichment pass (richer snippets and passages per result) | no | no | yes |
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 a round of server-side guessing.
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 just works; 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.
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 |
|---|
mode | echoes the request mode: fast, standard, or research |
ranker_version | first_stage_order_v1 (first-stage provider 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, modes, or ranker versions. Do not persist scores as absolute relevance.
Replayed searches may be served from cache; deterministic ordering, stable scores, and identical search_id values are not guaranteed across calls.
Reserved request fields
These fields are schema-valid and accepted today, but not forwarded into retrieval. Sending them is harmless; expecting behavior from them is a bug.
| Field | Status |
|---|
objective | reserved — intended to let agents shape retrieval with a task objective |
client_model | reserved — calling-model identifier for analytics and tuning |
content | reserved — content-return controls have no effect on /v1/search |
debug | reserved — internal evaluation only |
A full request
Every implemented request field in one call (the response block is covered in response shaping):
curl -s https://search-api-staging-779189860552.europe-west1.run.app/v1/search \
-H "Content-Type: application/json" \
-d '{
"query": "postgres 17 logical replication failover",
"search_queries": ["postgres 17 failover slot synchronization"],
"mode": "research",
"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:
{
"request_id": "5f0c2c4e-1d2a-4f6b-9e0a-7c1b2d3e4f50",
"search_id": "a7e2b9c1-3d4f-4a5b-8c6d-9e0f1a2b3c4d",
"session_id": "b1c2d3e4-f5a6-4b7c-8d9e-0f1a2b3c4d5e",
"access": { "tier": "api_key", "rate_limit": { "limit_rps": 100, "remaining": 99, "reset_at": "2026-06-12T10:00:01Z" } },
"ranking": { "mode": "standard", "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, "approx_tokens": 537 }
}
Notes on the envelope:
search_id is the handle for /v1/feedback. session_id echoes yours or is server-generated — see sessions.
- Per-result
provenance (capture_id, capture_time) appears only at response.verbosity: "full" — the default standard omits it. See response shaping.
usage.approx_tokens is bytes_returned divided by 4, rounded up — a documented estimate, not a tokenizer count.
- Error responses use a shared envelope with stable machine codes — see errors. Full field schemas live in the API reference.