Mistral

Document AI and OCR behind one fixed route, priced per page
View as Markdown

Reads PDFs and images through Mistral AI’s Document AI endpoint (https://api.mistral.ai), returning per-page markdown and — on the same call — structured fields pulled out with a JSON schema you supply. One route is mounted:

POST /mistral/v1/ocr → https://api.mistral.ai/v1/ocr

Mistral’s API is a whole platform on one shared account key — chat and FIM completions, embeddings, Files, Batch, Fine-tuning, Models, Agents, billing and usage. Only the OCR endpoint above is mounted, as a fixed literal route rather than a wildcard, so every other Mistral path draws the gateway’s own 404/mistral/v1/ocr/ (trailing slash) and /mistral/ocr included, as is any method other than POST.

Files, Batch, chat and Document QnA are not exposed. Uploads (/v1/files) and the file_id handle they mint are never reachable: one shared Mistral key backs this route, so a file handle usable by one client would be usable by every client — send a URL instead. Batch, Fine-tuning, Agents, embeddings, and the billing/usage endpoints are unmounted for the same shared-account reason. Document QnA — Mistral’s /v1/chat/completions with a document reference — is out of scope here as well: it bills per token rather than per page. For chat inference, use the Inference API or the individual model routes.

Like Browserless and Steel, this route is policed: a closed body policy runs before Mistral is called, and the request the gateway forwards is rebuilt from the fields that policy admits — nothing can ride along inside a nested object. Two things make it stricter still. pages is required, because cost scales with the document; and the price of the selection is checked against your balance before the upstream call, so an unaffordable job is refused rather than forwarded.

Credential handling

  • Authorization: Bearer is injected server-side with the gateway’s Mistral key. Your gateway token never goes upstream.
  • The upstream request is built from a fresh two-header envelopeauthorization and content-type, nothing else. Your header bag is dropped wholesale rather than filtered, so no Mistral key, account or project selector, or cost-affecting header of your own reaches the upstream.
  • Query parameters are rejected outright. Every control travels in the JSON body.

Request policy

The body is a closed allow-set of at most 16,384 bytes. Fields not named below are rejected with a 400 before Mistral is called.

FieldRequiredWhat it accepts
modelnoPinned to mistral-ocr-4-0. Spell it out or omit it.
documentyesA document_url chunk (with optional document_name) or an image_url chunk — see below.
pagesyesAt most 100 unique, zero-based page indices, as an array or a range string.
table_formatno"markdown", "html", or null (Mistral’s own default).
extract_header, extract_footernoBooleans.
confidence_scores_granularityno"page" or null.
document_annotation_formatnoA json_schema format for whole-document fields — see Structured extraction.
bbox_annotation_formatnoA json_schema format for per-figure labels.
document_annotation_promptno1–2,000 characters of plain-language instructions.

The document reference

Mistral fetches the document itself, from its own network, at the URL you give it. Two of Mistral’s three chunk types are admitted:

{ "type": "document_url", "document_url": "https://files.example.com/msa-2026.pdf", "document_name": "msa-2026.pdf" }
{ "type": "image_url", "image_url": "https://files.example.com/receipt.png" }

Both URLs must be public https — signed, time-limited links are the intended use. Refused: any other scheme (including http and inline data:), an embedded user:password, an IP-literal host in any spelling, and a single-label or .localhost/.local/.internal hostname. document_name is an optional label Mistral echoes back: a plain filename of at most 256 characters, no spaces or path separators.

Selecting pages

pages is required and zero-based — page one of a PDF is index 0. Both of Mistral’s spellings work:

{ "pages": [0, 1, 2] }
{ "pages": "0-4,9" }

A request may select at most 100 unique pages, each index at most 9999. A repeated or overlapping selection is refused rather than quietly deduplicated, and so is a descending range or an empty selection — a request should cost exactly what it looks like it costs. Mistral’s own default is the entire document, which has no cost ceiling; naming the pages up front is what lets the gateway price the call before running it.

Denied — a 400 before the request reaches Mistral: image_limit and image_min_size (they expand the response with image bytes the gateway neither meters nor buffers), the word value for confidence_scores_granularity, a file_id document chunk, an aliased model such as mistral-ocr-latest, any query parameter, and anything else the policy doesn’t name. include_blocks and include_image_base64 are always forwarded as false; asking for true is rejected rather than silently rewritten.

A rejected call answers 400 with {"error": "Rejected: <reason>."} — Mistral never sees it, and it’s never billed.

OCR a document

curl -X POST "$GATEWAY/mistral/v1/ocr" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"document": {
"type": "document_url",
"document_url": "https://files.example.com/contracts/msa-2026.pdf",
"document_name": "msa-2026.pdf"
},
"pages": [0, 1],
"table_format": "markdown"
}'

Mistral’s answer comes back exactly as it arrived — status, headers, and body bytes:

{
"pages": [
{ "index": 0, "markdown": "# Master Services Agreement\n\n...", "images": [] },
{ "index": 1, "markdown": "## 2. Fees\n\n| Item | Amount |\n|---|---|\n| Platform | $4,000 |", "images": [] }
],
"model": "mistral-ocr-4-0",
"document_annotation": null,
"usage_info": { "pages_processed": 2, "doc_size_bytes": 182734 }
}

images[] carries each detected figure and its bounding box; image_base64 is always null, since the gateway forwards include_image_base64: false.

Structured extraction

Add an annotation format and the same pass also returns fields shaped by your JSON schema — document_annotation_format for the document as a whole, bbox_annotation_format for each detected figure. Each format takes Mistral’s own shape (type: "json_schema" plus a json_schema object carrying name, schema, and optionally description and strict) and may be at most 8,192 bytes serialized.

curl -X POST "$GATEWAY/mistral/v1/ocr" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"document": { "type": "document_url", "document_url": "https://files.example.com/invoices/inv-8842.pdf" },
"pages": [0],
"document_annotation_format": {
"type": "json_schema",
"json_schema": {
"name": "invoice",
"strict": true,
"schema": {
"type": "object",
"properties": {
"invoice_number": { "type": "string" },
"total_due": { "type": "string" }
},
"required": ["invoice_number", "total_due"]
}
}
},
"document_annotation_prompt": "Return the invoice number and the total amount due."
}'

The extracted fields arrive in document_annotation (and per-figure results in each page’s images[].image_annotation), alongside the page markdown. json_schema.name is 1–64 characters of letters, digits, _, or -.

Either annotation format prices the whole call at the Document AI rate below. document_annotation_prompt on its own does not — it steers an annotation pass, it doesn’t request one.

Billing

Billed per page, not per call, on Mistral’s own published prices:

What the request asks forRate
OCR — page markdown$4 per 1,000 pages
Document AI — any request carrying document_annotation_format or bbox_annotation_format$5 per 1,000 pages

The annotated rate replaces the OCR rate for that call; the two never stack. Before the upstream request, the selected page count times the applicable rate is compared with your remaining balance — a job that doesn’t fit answers 402 naming the price and the page count (Insufficient credits for this request: 12000 µc for 3 selected page(s). Top up or select fewer pages.) and is never forwarded. A rejected or refused call is never charged.

Settlement happens only on a 2xx, from the usage_info.pages_processed Mistral itself reports — so a document whose selected pages turn out to be fewer than requested is charged for what was processed.

There is no retry on this route. Mistral documents no idempotency key for the OCR endpoint, so a silent second attempt could OCR — and bill — the same document twice. Retrying after a failure or timeout is your call, and counts as a new request.

Privacy

OCR input tends to be contracts, invoices, IDs, and medical records, so both log records this route writes keep metadata only — method, path, headers, status, and latency. Request and response bodies are never stored, which means neither the document URL (it travels in the body) nor a line of extracted text lands in a log. Usage records keep the operation, page counts, and rate — never document content.

Field-level request and response detail lives in Mistral’s OCR documentation; the gateway changes neither beyond the request policy described above.