> For clean Markdown of any page, append .md to the page URL.
> For a complete documentation index, see https://docs.nativeport.ai/llms.txt.
> For AI client integration (Claude Code, Cursor, etc.), connect to the MCP server at https://docs.nativeport.ai/_mcp/server.

# Mistral

Reads PDFs and images through [Mistral AI](https://docs.mistral.ai/capabilities/OCR/basic_ocr)'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](/inference) or the individual model routes.

Like [Browserless](/browserless) and [Steel](/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 envelope** — `authorization` 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.

| Field                              | Required | What it accepts                                                                                         |
| ---------------------------------- | -------- | ------------------------------------------------------------------------------------------------------- |
| `model`                            | no       | Pinned to `mistral-ocr-4-0`. Spell it out or omit it.                                                   |
| `document`                         | **yes**  | A `document_url` chunk (with optional `document_name`) or an `image_url` chunk — see below.             |
| `pages`                            | **yes**  | At most 100 unique, zero-based page indices, as an array or a range string.                             |
| `table_format`                     | no       | `"markdown"`, `"html"`, or `null` (Mistral's own default).                                              |
| `extract_header`, `extract_footer` | no       | Booleans.                                                                                               |
| `confidence_scores_granularity`    | no       | `"page"` or `null`.                                                                                     |
| `document_annotation_format`       | no       | A `json_schema` format for whole-document fields — see [Structured extraction](#structured-extraction). |
| `bbox_annotation_format`           | no       | A `json_schema` format for per-figure labels.                                                           |
| `document_annotation_prompt`       | no       | 1–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:

```json
{ "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:

```json
{ "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

```bash
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:

```json
{
  "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.

```bash
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 for                                                                   | Rate                    |
| ------------------------------------------------------------------------------------------- | ----------------------- |
| 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](https://docs.mistral.ai/capabilities/OCR/basic_ocr); the gateway changes neither beyond the request policy described above.