SFOLN

Catalogue / Documents & media

PDF OCR to Markdown Converter

sfoln/pdf-ocr-markdown · AI, Developer tools, Automation On request

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.

About

What it does

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

Price per delivered record

EventPricePer 1,000Charged for
OCR page
ocr-page
$0.007$7 / 1,000Charged 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,000Charged once per PDF page read from the PDF's own text layer (born-digital or blank pages, hybrid mode): no OCR needed.

Apify free plan: up to 5 items per run. Paid plans have no per-run cap; set maxTotalChargeUsd to cap spend.

Run

Call it from code, an agent or the Console

This API is private on Apify for now. Email us and we will publish it for you, usually within two working days. The calls below work once it is public. contact@sfoln.com

curl · synchronous run, rows in the response
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.json · hosted Apify MCP server
{
  "mcpServers": {
    "apify": {
      "type": "http",
      "url": "https://mcp.apify.com?tools=sfoln/pdf-ocr-markdown"
    }
  }
}
asynchronous run · for large batches
POST https://api.apify.com/v2/actors/sfoln~pdf-ocr-markdown/runs?maxTotalChargeUsd=1
GET  https://api.apify.com/v2/actor-runs/{runId}            # until status is SUCCEEDED
GET  https://api.apify.com/v2/datasets/{defaultDatasetId}/items?format=json

Authenticate with an Apify API token. MCP clients can instead sign in with OAuth when prompted.

Input

Input fields

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

FieldTypeDefaultMeaning
documentUrls *arrayrequiredDocument 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.
maxPagesPerDocumentinteger50Max pages per document
Only the first N pages of each document are processed (and charged). Range 1–1000.
includeImagesbooleanfalseExtract 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.
forceOcrbooleanfalseOCR 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.
maxDocumentsinteger100Max documents
Safety cap on the number of documents processed in this run. Range 1–10000.
concurrencyinteger2Concurrency
Documents processed in parallel. Range 1–5.
includeRawbooleanfalseInclude raw upstream response
Attach the unmodified OCR response (without image data) to each record.
Example input
{
  "documentUrls": [
    "https://www.w3.org/WAI/ER/tests/xhtml/testfiles/resources/pdf/dummy.pdf"
  ],
  "maxPagesPerDocument": 5
}

Output

One record per input item

Records land in the run's dataset; a synchronous call returns them directly. Each has a status (ok is charged; everything else is free) and a charged flag. The run's OUTPUT record totals items and charges.

FieldTypeMeaning
inputstringInput
URL as submitted.
urlstringURL
Normalized document URL.
documentTypestringDocument type
document (PDF, DOCX, PPTX, …) or image.
modestring | nullMode
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).
pageCountintegerPages returned
Pages returned.
totalPagesinteger | nullPages in the PDF
Hybrid mode: the PDF's page count.
pagesProcessedintegerPages processed
Pages processed: OCR'd by Mistral, or read from the text layer (hybrid).
pagesChargedintegerPages charged
Pages charged (ocr-page and pdf-text-page); exactly these pages are returned.
ocrPagesChargedinteger | nullOCR pages charged
ocr-page events for this document.
textPagesChargedinteger | nullText-layer pages charged
pdf-text-page events for this document.
textLayerPagesarray | nullText-layer pages
Hybrid mode: page numbers read from the PDF's text layer.
ocrPagesarray | nullOCR pages
Hybrid mode: page numbers sent to OCR.
pageLimitintegerPage limit applied
possiblyTruncatedbooleanPossibly truncated
The page limit was reached; the document may have more pages.
markdownstringMarkdown
All pages joined, separated by <!-- page N --> comments.
markdownUrlstring | nullFull 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.
pagesarrayPages
[{ index, pageNumber, markdown, charCount, imageCount, tableCount, source: text-layer | ocr | ocr-missing, images? }]
charsExtractedintegerCharacters extracted
imageCountintegerImages detected
docSizeBytesinteger | nullDocument size (bytes)
modelstring | nullOCR model
Mistral OCR model; null when no page needed OCR.
warningsarrayWarnings
statusstringStatus
ok | not_found | error | skipped_invalid | skipped_budget | skipped_free_plan e.g. "ok"
chargedbooleanCharged
sourcestringSource
fetchedAtstringFetched at
errorobject | nullError
{ code, message, provider?, status? } when status is not ok.
chargedEventstring | nullCharged event
Event charged by the runner for the record's last page (ocr-page or pdf-text-page).
Example record
{
  "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

Questions

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.

Machine-readable

This page for agents