The Document QnA API
On this page
Lesson updated November 3, 2026. Addendum November 3, 2026: To send a local PDF, transmit it as base64 in the format
{"type": "document_url", "document_url": "data:application/pdf;base64,…"}. Thedocument_base64type found in older examples does not exist in the documentation or themistralaiSDK, and those examples would have failed. Sources: OCR Processor · Document QnA.
Query your documents in natural language
Document QnA combines OCR with a Mistral LLM, letting you ask questions directly about your document content. Instead of scanning fifty pages to find a specific clause or figure, you simply ask the question and read the answer. Internally, the service runs two steps: OCR extracts the text, structure, and formatting, then a Mistral chat model analyzes the extracted content to generate a response. This combination is transparent to you—a single API call handles everything.
A different endpoint than OCR
This is the part that often trips people up on the first try. Pure OCR uses client.ocr.process, whereas QnA uses the standard Chat endpoint client.chat.complete, with a message containing both a text block and a document_url block. In other words, the document becomes an attachment to your question, just like an image in a multimodal exchange.
import os
from mistralai.client import Mistral
client = Mistral(api_key=os.environ["MISTRAL_API_KEY"])
# Question about a public document
response = client.chat.complete(
model="mistral-small-latest",
messages=[
{
"role": "user",
"content": [
{
"type": "text",
"text": "What is the total revenue mentioned?"
},
{
"type": "document_url",
"document_url": "https://example.com/annual-report.pdf"
}
]
}
]
)
print(response.choices[0].message.content)
Three ways to send the document
A public URL is the most direct route, ideal for documents already accessible online—such as a scientific publication, an institutional report, or a PDF hosted on your website.
messages = [
{
"role": "user",
"content": [
{"type": "text", "text": "Summarize the main points."},
{
"type": "document_url",
"document_url": "https://arxiv.org/pdf/1805.04770"
}
]
}
]
response = client.chat.complete(
model="mistral-small-latest",
messages=messages
)
Local files, on the other hand, are sent as base64. This is the default mode whenever a document is confidential or stored behind your firewall; nothing needs to be published on the web to be queried.
import base64
with open("contract.pdf", "rb") as f:
doc_base64 = base64.b64encode(f.read()).decode("utf-8")
messages = [
{
"role": "user",
"content": [
{
"type": "text",
"text": "What are the termination clauses?"
},
{
"type": "document_url",
"document_url": f"data:application/pdf;base64,{doc_base64}"
}
]
}
]
response = client.chat.complete(
model="mistral-small-latest",
messages=messages
)
Finally, there is the third option for files already uploaded via the Files API: you upload once with purpose="ocr", then reference the resulting ID as many times as needed. For a document you plan to query multiple times, this avoids re-uploading several megabytes with every question.
uploaded = client.files.upload(
file=open("report.pdf", "rb"),
purpose="ocr"
)
messages = [
{
"role": "user",
"content": [
{"type": "text", "text": "What is the planned budget?"},
{"type": "file_id", "file_id": uploaded.id}
]
}
]
response = client.chat.complete(
model="mistral-small-latest",
messages=messages
)
Which chat model for which question
The model parameter is up to you, and it impacts both answer quality and cost.
| Model | Profile | When to choose it |
|---|---|---|
mistral-small-latest | Fast and economical | Sufficient for most questions |
mistral-medium-latest | Good compromise | Balanced speed and accuracy |
mistral-large-latest | More precise | Complex analysis and reasoning |
Your choice depends on the complexity of your questions and your budget: extracting an amount from an invoice doesn’t require the same model as comparing liability regimes between two contracts.
Chaining questions
You don’t have to ask everything at once. You send the document in the first message, add the model’s response to the history, then ask your follow-up question as text only: the document context remains in the conversation. This is the natural way to explore a report—start with a summary, then dive deeper into the point that interests you.
history = [
{
"role": "user",
"content": [
{"type": "text", "text": "Summarize this document in 3 points."},
{
"type": "document_url",
"document_url": "https://example.com/report.pdf"
}
]
}
]
# First call
response = client.chat.complete(
model="mistral-small-latest",
messages=history
)
# Add response to history
history.append({
"role": "assistant",
"content": response.choices[0].message.content
})
# Follow-up question
history.append({
"role": "user",
"content": "Detail the second point."
})
response = client.chat.complete(
model="mistral-small-latest",
messages=history
)
print(response.choices[0].message.content)
What QnA won’t do for you
Four limitations are worth knowing before building a service on top of this. First, size: 50 MB per file and a maximum of 1,000 pages, which rules out large archives without prior splitting. Second, cost, since long documents consume many input tokens with every question asked. Third, the lack of persistent memory means you must reconstruct the history yourself; each call is independent outside of a multi-turn conversation. The trickiest point is the last one: the LLM interprets as much as it extracts, which can introduce errors in precise figures. For an amount going into accounting, prefer structured annotation; reserve QnA for understanding and synthesis.
Key takeaways
- Document QnA uses the Chat endpoint with
document_urlcontent: either a public URL or a base64-encoded PDF as a data URL - The internal workflow is OCR then LLM, but only one call is required
- Three sending methods: URL, base64, uploaded file
- Multi-turn conversations enable in-depth analysis
- Choose your chat model based on the complexity of your questions