Docsfra
API Documentation
v1

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
ReturnsList of chunk resultsSynthesized answer text + source list
Mode selectionmode: hybrid | semanticAlways hybrid
CostLower (retrieval only)Higher (retrieval + LLM)
WhenBuilding your own RAG flowA ready-made, synthesized answer is enough

How it works (retrieve -> generate)

  1. Retrievequery is sent to the same hybrid (vector + keyword, RRF) engine as /v1/search; top_k (default 5, maximum 20) chunks are fetched. If document_id is given, the search is scoped to a single document.
  2. 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.
  3. 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/completions endpoint (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, a RagError is thrown (→ 503).
  4. Answer — the text produced by the LLM is returned in the answer field. 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 in citations[], each carrying its source_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
}
FieldRequiredDescription
queryyesThe question being asked (cannot be empty)
top_knoNumber of chunks to fetch for context — default 5, maximum 20
document_idnoIf 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

HTTPcodeDescription
400invalid_requestquery missing/invalid, or document_id is an invalid opaque id
401unauthorizedMissing, invalid, or revoked key
402insufficient_creditBalance insufficient for the per-query charge — /ask costs more than /search (embed + LLM)
429rate_limitedtenant_ask throttle scope (60/min) exceeded
503rag_unavailableRAG 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.