# PDF OCR to Markdown Converter (`sfoln/pdf-ocr-markdown`)

Convert PDFs, scans and images to Markdown with Mistral OCR, tables and layout preserved. Born-digital PDF pages are read from their text layer for $1 per 1,000 pages; only scanned pages are OCR'd ($7 per 1,000). Per-page output.

- Family: Documents & media
- Categories: AI, Developer tools, Automation
- Availability: on request (private on Apify). Email contact@sfoln.com with subject "Access request: PDF OCR to Markdown Converter (sfoln/pdf-ocr-markdown)" before calling; calls fail until it is public.
- JSON: https://www.sfoln.com/actors/pdf-ocr-markdown.json
- MCP: https://mcp.apify.com?tools=sfoln/pdf-ocr-markdown
- REST (sync): POST https://api.apify.com/v2/actors/sfoln~pdf-ocr-markdown/run-sync-get-dataset-items
- REST (async): POST https://api.apify.com/v2/actors/sfoln~pdf-ocr-markdown/runs

Turn PDFs, scanned documents, slides and images into clean, structured Markdown, headings, lists, tables and reading order preserved, ready for RAG pipelines, LLM prompts, search indexing or data extraction. Paste document URLs, get one record per document with the full Markdown plus per-page text, character counts and (optionally) the embedded images. Scans and images are read by Mistral OCR, a document-understanding model that handles multi-column layouts, tables, handwriting and 40+ languages; PDF pages that already contain text are read straight from the PDF's text layer, at a seventh of the price.

## Price per delivered record

| Event | Price | Per 1,000 | Charged for |
| --- | --- | --- | --- |
| OCR page (`ocr-page`) | $0.007 | $7 / 1,000 | Charged once per page processed by Mistral OCR: scanned PDF pages, images, DOCX/PPTX pages, or every page with forceOcr. |
| Text-layer page (`pdf-text-page`) | $0.001 | $1 / 1,000 | Charged once per PDF page read from the PDF's own text layer (born-digital or blank pages, hybrid mode): no OCR needed. |

Pay-per-event, USD, billed by Apify; no platform usage charge. Only records with status `ok` are charged. Cap a run with `maxTotalChargeUsd`. Apify free plan: up to 5 per run.

## Run

```bash
curl -X POST "https://api.apify.com/v2/actors/sfoln~pdf-ocr-markdown/run-sync-get-dataset-items" \
  -H "Authorization: Bearer $APIFY_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"documentUrls":["https://www.w3.org/WAI/ER/tests/xhtml/testfiles/resources/pdf/dummy.pdf"],"maxPagesPerDocument":5}'
```

MCP client configuration:

```json
{
  "mcpServers": {
    "apify": {
      "type": "http",
      "url": "https://mcp.apify.com?tools=sfoln/pdf-ocr-markdown"
    }
  }
}
```


## Input

List publicly reachable document or image URLs. Each document produces one dataset record with its Markdown and per-page details.

| Field | Type | Default | Meaning |
| --- | --- | --- | --- |
| `documentUrls` (required) | array | — | Document URLs: Public URLs of PDFs, images (PNG, JPG, WEBP, TIFF, …) or office documents (DOCX, PPTX), one per line, max 50 MB each. PDF pages that have a text layer are read directly ($0.001 per page); scanned pages, images and office files are OCR'd ($0.007 per page). Apify key-value store record URLs work if the store is publicly readable. Accounts on the Apify free plan can process up to 5 documents per run, with at most 5 pages per document. |
| `maxPagesPerDocument` | integer | `50` | Max pages per document: Only the first N pages of each document are processed (and charged). |
| `includeImages` | boolean | `false` | Extract embedded images: Save images detected on the pages (figures, photos, logos) to the run's key-value store and return their URLs per page. Images come from OCR, so every page is OCR'd when this is on. |
| `forceOcr` | boolean | `false` | OCR every page: Send every PDF page to Mistral OCR, also pages that have a text layer (charged as ocr-page). Use it when you need OCR's layout and table reconstruction on born-digital pages too; images always need it. |
| `maxDocuments` | integer | `100` | Max documents: Safety cap on the number of documents processed in this run. |
| `concurrency` | integer | `2` | Concurrency: Documents processed in parallel. |
| `includeRaw` | boolean | `false` | Include raw upstream response: Attach the unmodified OCR response (without image data) to each record. |

Example input:

```json
{
  "documentUrls": [
    "https://www.w3.org/WAI/ER/tests/xhtml/testfiles/resources/pdf/dummy.pdf"
  ],
  "maxPagesPerDocument": 5
}
```

## Output

One record per input item with `status` (`ok` is charged; `not_found`, `error`, `skipped_*` are free) and `charged`. The run stores an `OUTPUT` summary record.

| Field | Type | Meaning |
| --- | --- | --- |
| `input` | string | Input: URL as submitted. |
| `url` | string | URL: Normalized document URL. |
| `documentType` | string | Document type: document (PDF, DOCX, PPTX, …) or image. |
| `mode` | string | null | Mode: hybrid: PDF pages with a text layer read directly, only scanned pages OCR'd; ocr: every page OCR'd (images, office files, forceOcr, includeImages, or a PDF whose text layer could not be read). |
| `pageCount` | integer | Pages returned: Pages returned. |
| `totalPages` | integer | null | Pages in the PDF: Hybrid mode: the PDF's page count. |
| `pagesProcessed` | integer | Pages processed: Pages processed: OCR'd by Mistral, or read from the text layer (hybrid). |
| `pagesCharged` | integer | Pages charged: Pages charged (ocr-page and pdf-text-page); exactly these pages are returned. |
| `ocrPagesCharged` | integer | null | OCR pages charged: ocr-page events for this document. |
| `textPagesCharged` | integer | null | Text-layer pages charged: pdf-text-page events for this document. |
| `textLayerPages` | array | null | Text-layer pages: Hybrid mode: page numbers read from the PDF's text layer. |
| `ocrPages` | array | null | OCR pages: Hybrid mode: page numbers sent to OCR. |
| `pageLimit` | integer | Page limit applied |
| `possiblyTruncated` | boolean | Possibly truncated: The page limit was reached; the document may have more pages. |
| `markdown` | string | Markdown: All pages joined, separated by <!-- page N --> comments. |
| `markdownUrl` | string | null | Full Markdown URL: Set only when the record would exceed ~6 MB: the full Markdown is in the key-value store, `markdown` keeps the first 1 MB. |
| `pages` | array | Pages: [{ index, pageNumber, markdown, charCount, imageCount, tableCount, source: text-layer \| ocr \| ocr-missing, images? }] |
| `charsExtracted` | integer | Characters extracted |
| `imageCount` | integer | Images detected |
| `docSizeBytes` | integer | null | Document size (bytes) |
| `model` | string | null | OCR model: Mistral OCR model; null when no page needed OCR. |
| `warnings` | array | Warnings |
| `status` | string | Status: ok \| not_found \| error \| skipped_invalid \| skipped_budget \| skipped_free_plan |
| `charged` | boolean | Charged |
| `source` | string | Source |
| `fetchedAt` | string | Fetched at |
| `error` | object | null | Error: { code, message, provider?, status? } when status is not ok. |
| `chargedEvent` | string | null | Charged event: Event charged by the runner for the record's last page (ocr-page or pdf-text-page). |

Example record:

```json
{
  "input": "https://example.com/invoice-pack.pdf",
  "url": "https://example.com/invoice-pack.pdf",
  "documentType": "document",
  "mode": "hybrid",
  "pageCount": 3,
  "totalPages": 3,
  "pagesProcessed": 3,
  "pagesCharged": 3,
  "ocrPagesCharged": 1,
  "textPagesCharged": 2,
  "textLayerPages": [
    1,
    3
  ],
  "ocrPages": [
    2
  ],
  "pageLimit": 3,
  "possiblyTruncated": false,
  "markdown": "## Invoice cover letter\n\nPlease find the scanned invoice on the next page.\n\n<!-- page 2 -->\n\n# INVOICE 2026-042\n\n| Item | Amount |\n| --- | --- |\n| Widgets | 1,190.00 EUR |\n\n<!-- page 3 -->\n\n## Terms and conditions\n...",
  "markdownUrl": null,
  "pages": [
    {
      "index": 0,
      "pageNumber": 1,
      "markdown": "## Invoice cover letter\n...",
      "charCount": 160,
      "imageCount": 0,
      "tableCount": 0,
      "source": "text-layer"
    },
    {
      "index": 1,
      "pageNumber": 2,
      "markdown": "# INVOICE 2026-042\n...",
      "charCount": 412,
      "imageCount": 1,
      "tableCount": 1,
      "source": "ocr"
    },
    {
      "index": 2,
      "pageNumber": 3,
      "markdown": "## Terms and conditions\n...",
      "charCount": 140,
      "imageCount": 0,
      "tableCount": 0,
      "source": "text-layer"
    }
  ],
  "charsExtracted": 712,
  "imageCount": 1,
  "docSizeBytes": 25513,
  "model": "mistral-ocr-2512",
  "status": "ok",
  "charged": true,
  "chargedEvent": "ocr-page",
  "source": "mistral-ocr",
  "fetchedAt": "2026-10-14T10:00:00.000Z"
}
```

## FAQ

**Why are some pages cheaper than others?**

PDF pages that already contain text (exported from Word, LaTeX or a browser) are read straight from the PDF's text layer and cost $0.001 (pdf-text-page). Only pages without a usable text layer, such as scans and photos, are sent to Mistral OCR and cost $0.007 (ocr-page). Set forceOcr to OCR every page.

**Does it handle tables and multi-column layouts?**

OCR pages keep tables as Markdown tables and follow the reading order of multi-column layouts. Text-layer pages rebuild headings, lists and simple tables from the PDF's layout; use forceOcr when you need OCR's reconstruction on every page.


## Related

- [PDF Text Extractor – PDF to Text & Markdown](https://www.sfoln.com/actors/pdf-text-extractor.md) (`sfoln/pdf-text-extractor`)
- [Document Parser – AI PDF & Image to JSON](https://www.sfoln.com/actors/document-to-json.md) (`sfoln/document-to-json`)
- [Document to Markdown – DOCX, PPTX, XLSX](https://www.sfoln.com/actors/document-to-markdown.md) (`sfoln/document-to-markdown`)
- [Audio Transcription API – Speech to Text](https://www.sfoln.com/actors/audio-transcription.md) (`sfoln/audio-transcription`)

Publisher: SFOLN LLC, contact@sfoln.com. Catalogue: https://www.sfoln.com/catalog.json. Agent guide: https://www.sfoln.com/agents.md.
