---
title: "One-round-trip passages + memories + citations retrieval (T386)"
method: POST
path: "/api/retrieve"
tags: ["Retrieve"]
---

# One-round-trip passages + memories + citations retrieval (T386)

`POST /api/retrieve`

## Request

- Base URL: `http://localhost:38200`
- URL: `http://localhost:38200/api/retrieve`
- Auth: one of:
  - HTTP bearer
  - API key in cookie `token`

The chatbot-facing retrieval primitive: given a natural-language query, returns ranked `passages` (chunk-level, never subject-scoped) and a separate `memories` array (subject-isolated when `subjectId` is given). Response shape is frozen — do not rely on undocumented fields disappearing being a breaking change; they are additive-only.

## Request body

- object
  - `query` string, required
  - `workspace` string: Workspace slug. Falls back to X-Workspace header, then the persisted active scope.
  - `project` string
  - `tags` string[]
  - `top_k` integer
  - `min_score` number
  - `subjectId` string: Scopes the `memories` array (not `passages`) to one end-user identity.
  - `include_memories` boolean
  - `mmr_lambda` number: T401: relevance/diversity tradeoff for the MMR pass over candidate passages. 1.0 = pure relevance, 0.0 = pure diversity. Omit for the unchanged v1 default.
  - `max_passages_per_doc` integer: T401: caps how many of the returned `passages` may come from the same document. Omit for the unchanged v1 default (2); set to 1 to force one-passage-per-document diversity.
  - `as_of` string, date-time: T690: point in VALID time (bi-temporal `valid_from`/ `valid_to`) to retrieve as of, instead of the current moment. Omit for "now". Mirrors POST /api/search's `as_of` -- see that param's own description.
  - `token_budget` integer: T697: ceiling on the response's `tokens_total`. Omit for no budget (the default; nothing is trimmed). `top_k` alone does NOT bound payload size -- an unchunked context is returned as one whole-content passage and chunk size is configurable per tenant -- so this is the only field that does. Enforcement is POST-RANKING and deterministic: the lowest-scored `passages` are dropped until the total fits, survivors keep their original relative order, and ranking itself is never recomputed. `memories` and `groupMemories` are COUNTED toward the total but are NEVER trimmed -- they are subject-scoped personalization, so a budget smaller than they alone occupy drops every passage and still returns `tokens_total` above the budget rather than truncating them. The number of passages removed is always reported in `dropped_count`.
  - `latency_budget_ms` integer: T700: cross-stage wall-clock budget for the WHOLE request. Omit for unchanged behaviour -- this is strictly opt-in and nothing pays for it by default. Distinct from the per-stage timeouts (`SEARCH_QUERY_REWRITE_TIMEOUT_MS` default 6000, `SEARCH_RERANKING_TIMEOUT_MS` default 8000), which are independent and ADDITIVE -- 14 s of stage timeout alone, before PG/ES/vector run -- so no combination of them can express "answer within a second". Before each optional heavy stage (query-rewrite, graph-assist, cross-encoder rerank) the elapsed time is compared against the budget; a stage with too little room left is SKIPPED WITHOUT BEING ENTERED (no network call at all) and named in `searchMeta.degradedStages[]`. A stage that IS admitted has its own timeout clamped down to the remainder. The mandatory legs are never skipped -- a budget shapes which optional quality stages run, it is not a request-abort deadline, so the response is always well-formed.

## Response `200`

Passages, memories, group memories, and a retrieval trace id for the query.

- object: Response shape is frozen -- `passages`/`memories`/ `groupMemories` are additive-only (new per-passage fields, never a removed/renamed one). additionalProperties stays true on each passage item so an older SDK mirror tolerates a server that has moved ahead of it.
  - `passages` RetrievePassage[]
    - `text` string
    - `score` number
    - `tokens` integer: T697: exact token count of `text` (cl100k_base). Read from the stored `context_chunks.token_count` for a chunk-backed passage, and counted at read time for the whole-document, memory and group-memory paths, where no chunk row exists. Always present on a T697-or-later server; treat as optional only when talking to an older one.
    - `citation` object
      - `source_type` string
      - `title` string
      - `external_id` string, nullable
      - `heading_path` string, nullable
      - `page` integer
      - `sheet` string
      - `table` string
      - `row_start` integer
      - `row_end` integer
      - `url` string
    - `contextId` integer
    - `updatedAt` string, date-time, nullable: ISO timestamp the underlying context was last updated.
    - `validFrom` string, date-time, nullable: Bi-temporal - when the fact this context describes started being true (null = unknown).
    - `validTo` string, date-time, nullable: Bi-temporal - when the fact stopped being true (null = still valid).
    - `confidence` number, nullable: Belief confidence for the underlying context, or null when unavailable.
    - `epistemicStatus` 'observed' | 'told' | 'inferred' | 'assumed', nullable: Write-time confidence tag.
    - `transformType` 'manual' | 'ingest' | 'dream_synthesis' | 'evolve' | 'agent_write', nullable: Most recent provenance transform type, or null when no provenance row exists yet or the best-effort lookup failed.
  - `memories` RetrievePassage[]
    - `text` string
    - `score` number
    - `tokens` integer: T697: exact token count of `text` (cl100k_base). Read from the stored `context_chunks.token_count` for a chunk-backed passage, and counted at read time for the whole-document, memory and group-memory paths, where no chunk row exists. Always present on a T697-or-later server; treat as optional only when talking to an older one.
    - `citation` object
      - `source_type` string
      - `title` string
      - `external_id` string, nullable
      - `heading_path` string, nullable
      - `page` integer
      - `sheet` string
      - `table` string
      - `row_start` integer
      - `row_end` integer
      - `url` string
    - `contextId` integer
    - `updatedAt` string, date-time, nullable: ISO timestamp the underlying context was last updated.
    - `validFrom` string, date-time, nullable: Bi-temporal - when the fact this context describes started being true (null = unknown).
    - `validTo` string, date-time, nullable: Bi-temporal - when the fact stopped being true (null = still valid).
    - `confidence` number, nullable: Belief confidence for the underlying context, or null when unavailable.
    - `epistemicStatus` 'observed' | 'told' | 'inferred' | 'assumed', nullable: Write-time confidence tag.
    - `transformType` 'manual' | 'ingest' | 'dream_synthesis' | 'evolve' | 'agent_write', nullable: Most recent provenance transform type, or null when no provenance row exists yet or the best-effort lookup failed.
  - `groupMemories` RetrievePassage[]: T414: contexts tagged to any group `subjectId` is a verified member of. Empty when no `subjectId` was supplied, or the subject belongs to zero groups.
    - `text` string
    - `score` number
    - `tokens` integer: T697: exact token count of `text` (cl100k_base). Read from the stored `context_chunks.token_count` for a chunk-backed passage, and counted at read time for the whole-document, memory and group-memory paths, where no chunk row exists. Always present on a T697-or-later server; treat as optional only when talking to an older one.
    - `citation` object
      - `source_type` string
      - `title` string
      - `external_id` string, nullable
      - `heading_path` string, nullable
      - `page` integer
      - `sheet` string
      - `table` string
      - `row_start` integer
      - `row_end` integer
      - `url` string
    - `contextId` integer
    - `updatedAt` string, date-time, nullable: ISO timestamp the underlying context was last updated.
    - `validFrom` string, date-time, nullable: Bi-temporal - when the fact this context describes started being true (null = unknown).
    - `validTo` string, date-time, nullable: Bi-temporal - when the fact stopped being true (null = still valid).
    - `confidence` number, nullable: Belief confidence for the underlying context, or null when unavailable.
    - `epistemicStatus` 'observed' | 'told' | 'inferred' | 'assumed', nullable: Write-time confidence tag.
    - `transformType` 'manual' | 'ingest' | 'dream_synthesis' | 'evolve' | 'agent_write', nullable: Most recent provenance transform type, or null when no provenance row exists yet or the best-effort lookup failed.
  - `traceId` string, nullable: Retrieval-trace row id, or null when RETRIEVAL_TRACE_ENABLED is off (the default).
  - `tokens_total` integer: T697: sum of every entry's `tokens` across `passages`, `memories` and `groupMemories`, as actually returned (i.e. after any `token_budget` trim). Lets a caller size its prompt without re-tokenising the payload.
  - `dropped_count` integer: T697: how many `passages` a `token_budget` removed. Always present; `0` when no budget was supplied or nothing had to go. Never counts `memories`/ `groupMemories`, which are never dropped.
  - `searchMeta` object: T700: latency + degradation observability for the doc-level candidate search plus this endpoint's own MMR pass. Always present. `degradedStages` is the one field to read to tell a deliberately fast path (a `latency_budget_ms` that skipped rerank) from a silent quality regression (a reranker that timed out); an empty array means the full pipeline ran.
    - `stageTimings` SearchStageTimings: T700: coarse wall-clock cost of each search stage, in milliseconds. Every field except `totalMs` is present only when that leg actually ran -- an ABSENT key means "did not run", which is deliberately distinct from `0` ("ran, was instant"). Observational only: nothing in ranking, fusion or filtering reads these, and they are kept out of the T368 signed retrieval certificate because non-deterministic data inside that envelope would make signature verification timing-dependent (they are persisted to the UNSIGNED `retrieval_traces.stage_timings` sidecar column instead).
      - `similarityCacheMs` integer: Similarity-cache lookup INCLUDING the query embed it performs. Separate from `vectorMs` because an APPLIED query-rewrite invalidates this vector and forces the chunk stage to embed a second time -- splitting them is what makes that visible.
      - `queryRewriteMs` integer
      - `graphSearchMs` integer
      - `pgMs` integer: Main PG ranked query plus its concurrent COUNT.
      - `esMs` integer: ES availability probe + BM25 query + RRF backfill.
      - `vectorMs` integer: Chunk-aware retrieval - embed (if not reused) + pgvector cosine scan.
      - `rerankMs` integer
      - `mmrMs` integer: T401 MMR selection. POST /api/retrieve only; never set by /api/search.
      - `totalMs` integer, required: Wall clock for the whole call. Always present.
    - `degradedStages` SearchDegradedStage[]
      - `stage` 'queryRewrite' | 'graphSearch' | 'elasticsearch' | 'chunkSearch' | 'rerank' | 'mmr', required
      - `reason` string, required: `budget_exhausted` means the stage was skipped before being entered because `latency_budget_ms` had no room left. Every other value passes through from the stage's own vocabulary (e.g. `timeout`, `rate_limited`, `http_5xx`, `llm_error`, `error`, `unavailable`).
      - `elapsedMs` integer, required: Wall clock already spent when the decision was taken.
      - `remainingBudgetMs` integer: Budget left at that moment. Absent when no `latency_budget_ms` was supplied (i.e. the entry describes a real failure, not a budget skip). Can be negative when an earlier stage already overran.

## Other responses

- `400`: Request body/query failed Zod validation.
- `401`: Missing or invalid credentials.

---

[API](https://skmtc.dev/contextq/apis/contextq-api.md) · [All operations](https://skmtc.dev/contextq/apis/contextq-api/llms.txt) · [OpenAPI document](https://skmtc.dev/contextq/apis/contextq-api/revisions/7afc44056a22?raw)
