Ask (RAG)
POST /v1/ask is a convenience layer that combines retrieval and LLM
synthesis in a single call: it takes your query, finds the relevant
chunks with /v1/search's same hybrid (RRF) engine, and returns a
synthesized, page-cited answer generated only from those chunks.
Difference from Search
/v1/search is a retrieval primitive — it returns a list of
chunks, it does not synthesize; it's the building block when you want
to build your own RAG flow (with your own prompt/LLM).
/v1/ask uses the same retrieval layer but also performs the
synthesis step for you: you get a ready-made, source-cited answer
text.
/v1/search | /v1/ask | |
|---|---|---|
| Returns | List of chunk results | Synthesized answer text + source list |
| Mode selection | mode: hybrid | semantic | Always hybrid |
| Cost | Lower (retrieval only) | Higher (retrieval + LLM) |
| When | Building your own RAG flow | A ready-made, synthesized answer is enough |
How it works (retrieve -> generate)
- Retrieve —
queryis sent to the same hybrid (vector + keyword, RRF) engine as/v1/search;top_k(default 5, maximum 20) chunks are fetched. Ifdocument_idis given, the search is scoped to a single document. - No context — if no chunks are found at all, the LLM is
never called; a fixed, honest answer is returned:
"No answer to this question was found in the documents."This call still counts as successful and is billed normally. - Generate — if chunks were found, each is turned into a context
block headed
[Source: document=..., page=..., section=...](blocks separated by---) and sent to an engine-agnostic, OpenAI-compatible/chat/completionsendpoint (low temperature,temperature=0.1). Transient errors (5xx/timeout) are retried with exponential backoff; on persistent errors (4xx) or if RAG is not configured server-side, aRagErroris thrown (→503). - Answer — the text produced by the LLM is returned in the
answerfield. Passages are numbered ([S1],[S2], ...) and the model cites each claim inline with its source number; only the sources actually cited in the answer are returned incitations[], each carrying itssource_no.
Auth
Like all M2M endpoints, requires an Authorization: Bearer <api_key>
header (dip_live_... or dip_test_..., see Getting Started and
Auth).
Request
Content-Type: application/json body:
{
"query": "what are the invoice payment terms?",
"top_k": 5,
"document_id": null
}
| Field | Required | Description |
|---|---|---|
query | yes | The question being asked (cannot be empty) |
top_k | no | Number of chunks to fetch for context — default 5, maximum 20 |
document_id | no | If given, the answer is scoped to a single document |
Response
{
"query": "what are the invoice payment terms?",
"answer": "...[document/page 3]...",
"citations": [
{
"document_id": "...",
"external_item_ref": "...",
"page_no": 3,
"section": "Payment Terms",
"chunk_id": "...",
"snippet": "..."
}
]
}
citations[] contains only the sources the answer actually cites
(matching the inline [S#] markers one-to-one via source_no) —
retrieved-but-unused chunks are no longer presented as sources. When
the honest answer is "not found in the documents", citations is
empty. Retrieval also applies a reranker relevance floor
(RERANK_MIN_SCORE): candidates the cross-encoder scores below the
floor never reach synthesis, so an unanswerable question doesn't
surface best-of-bad "sources". snippet is the first 200 characters
of the cited chunk's text.
Faithfulness (no hallucination)
The synthesis system prompt enforces these rules on the model: use
only the information in the given context, give an [S#]
style source citation after every significant claim, explicitly state when
the answer isn't in the context ("this information was not found in
the document"), never make things up. If no context (chunks) is
found at all, this rule doesn't even come into play — a fixed, honest
answer is returned without ever calling the LLM.
Credits and billing
/v1/ask costs 0.20 credits/query by default — more expensive
than /v1/search because it includes both the embed and the LLM
step. Balance is checked before the request (402 insufficient_credit if insufficient); if synthesis fails (503) no
credit is deducted, it's only deducted on a successful answer.
dip_test_... test keys are exempt from query charges (usage is
still measured, but not deducted from balance).
Errors
| HTTP | code | Description |
|---|---|---|
| 400 | invalid_request | query missing/invalid, or document_id is an invalid opaque id |
| 401 | unauthorized | Missing, invalid, or revoked key |
| 402 | insufficient_credit | Balance insufficient for the per-query charge — /ask costs more than /search (embed + LLM) |
| 429 | rate_limited | tenant_ask throttle scope (60/min) exceeded |
| 503 | rag_unavailable | RAG not configured (missing server-side config) or the LLM returned a persistent error — no credit is deducted in this case |
Notes
/v1/ask is a convenience endpoint that compresses the
retrieve+generate steps into a single call; if you want to use your
own prompt/LLM, you can build your own RAG flow yourself with
/v1/search.
Cross-check: contradictions between documents
POST /v1/cross-check compares the verified reasoning claims of two
or more processed documents (no upper limit — with many documents each
contributes its most important claims within a shared budget) against each other and returns cross-document
findings — contradictions ("document A says X, document B proves not-X")
and support pairs (an obligation in one document, its fulfilment
evidence in another).
{ "document_ids": ["d_...", "d_..."] }
The mechanism inherits the reasoning layer's hallucination shield: the
claims being compared were already verified against document text at
extraction time (each carries a page number and an exact quote), and
every finding the model proposes is validated deterministically — it
may only reference known claim ids, both sides must come from
different documents, and each side's statement/quote/page in the
response is filled from the server's own records, never from the
model's echo. An empty findings array is a valid answer; the model
is instructed not to force pairs it is unsure about.
Coverage is systematic, not attention-limited: candidate pairs are first
generated deterministically — every claim is embedded (on Docsfra's own
infrastructure) and matched against the most similar claims of the other
documents, plus any pair sharing a salient token (reference number, amount,
date) — and each candidate is then judged individually by the model
(contradiction / support / none). A separate free-scanning pass catches
rule-vs-violation pairs that similarity cannot surface (e.g. an
"all documents must be in English" clause vs a German sentence). All
channels merge by pair; results are stable across calls and returned in a
deterministic order (contradictions first). The response's stats object
reports how many claims and candidate pairs were examined.
{
"documents": [{ "document_id": "d_...", "label": "..." }],
"version": "xchk-v1",
"findings": [
{
"kind": "contradiction",
"explanation": "…",
"a": { "document_id": "d_...", "node_id": "g2-c2", "statement": "…", "quote": "…", "page_no": 4, "span": {"start": 120, "end": 180} },
"b": { "document_id": "d_...", "node_id": "g7-r1", "statement": "…", "quote": "…", "page_no": 27, "span": {"start": 40, "end": 130} }
}
]
}
Requirements and billing: every document must belong to your tenant and
have its reasoning layer generated (422 reasoning_not_available
otherwise; 404 document_not_found for unknown ids). The call is billed
as one ask query (same rate, same throttle scope, no charge on
503 crosscheck_unavailable). Also available in the Console under
Çapraz Denetim, and as the cross_check MCP tool.