PDF to Markdown for RAG & LLMs: OCR, DOCX, Excel avatar

PDF to Markdown for RAG & LLMs: OCR, DOCX, Excel

Pricing

from $2.00 / 1,000 documents

Go to Apify Store
PDF to Markdown for RAG & LLMs: OCR, DOCX, Excel

PDF to Markdown for RAG & LLMs: OCR, DOCX, Excel

Convert PDF (native or scanned), DOCX, XLSX, PPTX, HTML and images, by URL or upload, to Markdown, JSON or RAG chunks with page numbers and headings. Tesseract OCR with per-page confidence. $2 per 1,000 documents + $0.0003/page; failures are free. 50 files per run by default. MCP tool for AI agents.

Pricing

from $2.00 / 1,000 documents

Rating

0.0

(0)

Developer

Rigel

Rigel

Maintained by Community

Actor stats

0

Bookmarked

2

Total users

1

Monthly active users

17 hours ago

Last modified

Share

What is Document to Markdown for RAG?

PDF to Markdown, DOCX to Markdown and OCR in one Actor. Send PDFs (native or scanned), Word, Excel, PowerPoint, HTML or images by URL. You get back clean Markdown, structured JSON, or RAG chunks that carry page numbers and the heading path for citations.

  • Every page records how it was read (text-layer or ocr) and, for scans, Tesseract's confidence.
  • Tables become Markdown tables. A ZIP of CSV files per document is optional.
  • Failed, empty, duplicate and unsupported files are free. There is no start fee.
  • The file never leaves the run: there is no third-party LLM or OCR API.
  • It works from the API, Python, LangChain, LlamaIndex, n8n, Make, and as an MCP tool for AI agents (Claude, Cursor, any MCP client).

How much does it cost?

Pay per event. $2.00 per 1,000 documents, plus the pages actually read:

EventPriceWhen
Document$0.002A file that produced at least one billed page
Text page$0.0003A PDF page read from the text layer, one spreadsheet sheet, one slide, or 3,000 characters of Word, HTML, Markdown or plain text
OCR page$0.004A PDF page or image frame read with Tesseract. Replaces the text-page event for that page
Free$0Download failure, unsupported type, encrypted file, empty result, duplicate in the same run, pages skipped by pages

Real examples:

InputChargedCost
IRS Publication 509 (PDF, 14 text pages)1 document + 14 pages$0.0062
NIST CSF 2.0, pages 1–8, as RAG chunks1 document + 8 pages$0.0044
Word file under 3,000 characters1 document + 1 page$0.0023
Excel workbook, 2 sheets1 document + 2 pages$0.0026
One scanned page1 document + 1 OCR page$0.006
20-page text PDF / 20-page scan$0.008 / $0.082
1,000 ten-page text PDFs1,000 documents + 10,000 pages$5.00

Set a spending cap on the run (maxTotalChargeUsd, minimum $0.05). When the next event would go over it, the run stops cleanly and the row is partial with the flag truncated-budget. Events that were not delivered are not charged.

How to use it

  1. Paste one or more direct file URLs into Document URLs, or select files from your computer with Upload files. A path such as C:\report.pdf is not a URL and is rejected.
  2. Pick an Output format: markdown (one row per document), json (adds per-page Markdown and a plain text field), text, or chunks (one row per RAG chunk).
  3. Run. Download the dataset as JSON, CSV or Excel, or read OUTPUT.md in the key-value store when you converted a single document.

Ready-made examples: PDF chunks for RAG · PDF to Markdown for LLMs · Scanned PDF OCR · Image OCR · DOCX to Markdown · Excel tables · PPTX with notes · Batch PDF URLs

Output

One dataset row per document:

{
"source": "https://www.irs.gov/pub/irs-pdf/p509.pdf",
"fileName": "p509.pdf",
"mimeType": "application/pdf",
"status": "ok",
"pageCount": 14,
"pagesProcessed": 14,
"metadata": { "title": "2026 Publication 509", "language": "en" },
"markdown": "<!-- page: 1 -->\n\n## Publication 509\n\n# Contents\n\n…",
"pages": [ { "page": 1, "method": "text-layer", "ocrConfidence": null, "chars": 2446 } ],
"tables": [ { "page": 2, "rows": 5, "cols": 2, "markdown": "| IF you're... | THEN you may need... |\n| --- | --- | …", "csvKey": null } ],
"flags": [],
"charged": { "document": 1, "page": 14, "ocr-page": 0 }
}

With output: "chunks", you get one row per chunk instead:

{
"fileName": "NIST.CSWP.29.pdf",
"status": "ok",
"chunkIndex": 3,
"text": "…",
"pageStart": 2,
"pageEnd": 4,
"headingPath": ["The NIST Cybersecurity Framework (CSF) 2.0", "Abstract", "Acknowledgments"],
"chars": 1264,
"charged": false
}

The document is charged once. Only chunk 0 carries charged, metadata, pages, tables and pageCount; later chunks have "charged": false.

<!-- page: N --> marks where each source page starts (turn this off with includePageMarkers: false). The key-value store also holds RUN_SUMMARY.json (counts, error codes, amount charged), one doc-*.md file per document, OUTPUT.md when exactly one document was converted, and tables-*.zip when tableCsv is on. Markdown longer than 400,000 characters is cut in the row, and the full text is stored under markdownKey.

A file that fails still gets a row with status: "failed", an errorCode and charged: false. If every file fails, the run fails and nothing is charged.

errorCodeMeaning
DOWNLOADHTTP error, DNS failure or the 60-second download timeout
FILE_ACCESS_DENIEDThe key-value record refused the run. Pass the signed recordPublicUrl (?signature=), or use Upload files in the Console
SSRFPrivate, loopback or link-local address (also checked after redirects)
UNSUPPORTEDFile type not handled, including legacy .doc and .ppt
ENCRYPTEDPassword-protected PDF
EMPTYNo text found
CORRUPTThe file could not be parsed
CONTENT_MISMATCHAn HTML page saved under a .pdf or .docx name
TOO_LARGE / ZIP_BOMBOver maxFileMb, or an Office file that unpacks to more than 200 MB
TIMEOUTThe conversion did not finish within timeoutSecs
OCR_FAILEDTesseract could not read the page
PAGE_RANGEpages is invalid or selects no page of this document
LIMITThe spending cap was reached before this document
INPUT / INTERNALInvalid source item / unexpected error

Input

FieldDefaultRole
sources—{"url": "https://…"} items. A requestsFromUrl link list is accepted.
files—One or more uploaded files. In the Console, use Upload files. From the API, pass each record's signed recordPublicUrl. The record key becomes fileName.
datasetId—Optional dataset picker (read access). Each item is one source: a URL string or {"url": …}
keyValueStoreId—Optional key-value store picker (read access). datasetId items {"kvKey": "report.pdf"} are read from this store
outputmarkdownmarkdown, json, text or chunks
ocrautoauto OCRs a PDF page only when it has no text layer and holds a large image. always OCRs every PDF page, never uses the text layer only. Images are always OCR'd.
ocrLanguageseng, fraAlso deu, spa, ita, por, nld
maxOcrPages40OCR pages billed per document. Further scanned pages are skipped, free, and flagged truncated-ocr
ocrDpi200120–300
pagesallPage range for PDF, PowerPoint or TIFF, e.g. 1-5,8
tables, tableCsvtrue, falseMarkdown tables; optional ZIP of CSV files per document
excelOutputmarkdownXLSX/XLS only. rows writes one JSON item per data row (sheet, rowIndex, values) instead of the Markdown row; both returns both. A sheet is still billed as one text page
includePageMarkers, includeNotestrue, true<!-- page: N --> markers; PowerPoint speaker notes
chunkSize, chunkOverlap1500, 150Chunk budget in characters, overlap included. Long blocks are cut on a blank line, a sentence, then a word. Long tables are cut between rows and repeat their header row
maxPagesPerDocument500Beyond this, pages are skipped and flagged truncated-pages
maxRowsPerSheet20000Truncated sheets are flagged truncated-rows
maxFileMb, maxDocuments100, 50Up to 100 MB per file. Default 50 documents, maximum 1,000. A run stops at 3600 seconds
timeoutSecs300Per document. Pages already read are kept and charged; the rest are not
headers—HTTP headers for your own files (e.g. Authorization). Never logged, and dropped if a redirect changes host

Accepted: PDF, DOCX, XLSX, XLS, CSV, PPTX, HTML, TXT, Markdown, PNG, JPEG, TIFF, WebP, GIF, BMP.

Call it from the API

curl -X POST "https://api.apify.com/v2/acts/rigel~doc-to-markdown/run-sync-get-dataset-items?maxTotalChargeUsd=0.10" \
-H "Authorization: Bearer $APIFY_TOKEN" \
-H "Content-Type: application/json" \
-d '{"sources": [{"url": "https://www.irs.gov/pub/irs-pdf/p509.pdf"}], "output": "markdown"}'

run-sync-get-dataset-items waits for the run and returns HTTP 408 after 300 seconds. For large batches, start the run asynchronously (/runs) and read the dataset when it finishes, or use a webhook.

Upload a file

In the Console, open the input and use Upload files. Select one or more files. The Console stores each file in your key-value store and fills files with the signed record URL. The original file name is the record key, and that name is fileName in the dataset. Document URLs stays a list of {"url": "https://…"} and is unchanged. Billing is the same as for a document URL.

The Actor runs with limited permissions, so it cannot open your store with its own client. It GETs the URL you pass, including ?signature=, and does not follow a redirect or add a token. A signed record URL answers 200 without a token. If that GET returns 401 or 403 and the URL has no signature, the run then tries its own store only.

From the API, store the file, then pass recordPublicUrl. Listing keys returns that field on every record:

GET https://api.apify.com/v2/key-value-stores/{storeId}/keys

The bytes themselves are:

GET https://api.apify.com/v2/key-value-stores/{storeId}/records/{recordKey}?signature=…

from decimal import Decimal
from apify_client import ApifyClient
client = ApifyClient("<APIFY_TOKEN>")
store = client.key_value_store("<STORE_ID>")
with open("report.pdf", "rb") as handle:
store.set_record("report.pdf", handle.read(), content_type="application/pdf")
signed = store.get_record_public_url("report.pdf")
# https://api.apify.com/v2/key-value-stores/<STORE_ID>/records/report.pdf?signature=…
client.actor("rigel/doc-to-markdown").call(
run_input={"files": [signed]},
max_total_charge_usd=Decimal("0.10"),
)
curl -X PUT "https://api.apify.com/v2/key-value-stores/$STORE_ID/records/report.pdf" \
-H "Authorization: Bearer $APIFY_TOKEN" \
-H "Content-Type: application/pdf" \
--data-binary @report.pdf
SIGNED=$(python - <<'PY'
from apify_client import ApifyClient
import os
store = ApifyClient(os.environ["APIFY_TOKEN"]).key_value_store(os.environ["STORE_ID"])
print(store.get_record_public_url("report.pdf"))
PY
)
curl -X POST "https://api.apify.com/v2/acts/rigel~doc-to-markdown/runs?maxTotalChargeUsd=0.10" \
-H "Authorization: Bearer $APIFY_TOKEN" \
-H "Content-Type: application/json" \
-d "{\"files\":[\"$SIGNED\"]}"

Only https://api.apify.com/v2/key-value-stores/<store>/records/<file> is accepted on that host. Any other Apify API path is refused. The signature is not written to the dataset. Any other host goes through the same SSRF checks as sources.

Python, LangChain and LlamaIndex

from decimal import Decimal
from apify_client import ApifyClient
from langchain_core.documents import Document
client = ApifyClient("<APIFY_TOKEN>")
run = client.actor("rigel/doc-to-markdown").call(
run_input={
"sources": [{"url": "https://nvlpubs.nist.gov/nistpubs/CSWP/NIST.CSWP.29.pdf"}],
"output": "chunks",
"pages": "1-8",
"includePageMarkers": False,
},
max_total_charge_usd=Decimal("0.10"),
)
documents = [
Document(
page_content=row["text"],
metadata={
"source": row["source"],
"page_start": row["pageStart"],
"page_end": row["pageEnd"],
"heading_path": " > ".join(row.get("headingPath") or []),
},
)
for row in client.dataset(run.default_dataset_id).iterate_items()
if row.get("status") != "failed"
]

For LlamaIndex, build TextNode(text=row["text"], metadata={...}) from the same rows.

n8n and Make

  • n8n: an HTTP Request node POST https://api.apify.com/v2/acts/rigel~doc-to-markdown/run-sync-get-dataset-items with Header Auth Authorization: Bearer <token> and the input as JSON body. Or use the Apify node, Run an Actor and get dataset. Branch on status and errorCode.
  • Make: Apify › Run an Actor (Run synchronously: Yes) → Get Dataset Items → map markdown.

Use it from AI agents (MCP)

Add the Apify MCP server with only this tool:

{ "mcpServers": { "apify-docs": { "url": "https://mcp.apify.com?tools=rigel/doc-to-markdown" } } }

or claude mcp add --transport http apify-docs "https://mcp.apify.com?tools=rigel/doc-to-markdown". Sign in with OAuth, or add the header Authorization: Bearer <APIFY_TOKEN>.

The agent gets the tool rigel--doc-to-markdown, plus get-dataset-items and get-key-value-store-record to read the results. The tool returns run status and storage ids, not the text. Tell your agent to:

  1. call rigel--doc-to-markdown with output: "chunks" for long files, or markdown for short ones;
  2. read rows with get-dataset-items and fields: "pageStart,pageEnd,headingPath,text", or read OUTPUT.md for a single short document;
  3. cite pageStart–pageEnd.

This Actor is pay-per-event and runs with limited permissions, so agents can also use it through Apify's agentic payments.

FAQ

How much does it cost to convert 1,000 PDFs? $2.00 for the documents, plus $0.30 per 1,000 text pages. For 1,000 ten-page text PDFs that is $5.00. Scanned pages cost $0.004 each.

Do I pay for files that fail? No. Download errors, encrypted or unsupported files, empty results and duplicates are free, and a run where every file fails costs nothing.

Can it read scanned PDFs and photos? Yes. Pages without a text layer are OCR'd with Tesseract in English, French, German, Spanish, Italian, Portuguese or Dutch. Each page reports ocrConfidence. Clean scans score above 0.9 in our tests; handwriting does not work well.

Are my documents sent to OpenAI or another API? No. Conversion and OCR run inside your Apify run. Files are stored only in your own dataset and key-value store. Logs hold counters and error codes, never file contents, names or URLs.

How do I keep page numbers for citations? Use output: "chunks": each chunk has pageStart, pageEnd and headingPath. In markdown mode, <!-- page: N --> markers show where each page starts.

What does a "page" mean for Word or HTML? Word, HTML and text have no fixed pages, so they are billed and cited in 3,000-character slices. A spreadsheet sheet or a slide counts as one page.

Can I convert files that are not on a public URL? Use Upload files in the Console, or pass a signed recordPublicUrl. headers (for example Authorization) still work for your own file hosts. Private and local network addresses are refused, and this Actor never sends your token to a URL.

How large can a batch be? The default is 50 documents, and the maximum is 1,000, at 100 MB per file. The run itself stops after 3600 seconds. Documents already finished are kept and charged; documents not started are not. Raise maxDocuments only when each file is small, and run with /runs instead of run-sync.

Why is my .doc or .ppt refused? Legacy binary Office formats are not supported. Save the file as .docx or .pptx.

Does it parse invoices? No. When a Factur-X / ZUGFeRD invoice is detected, the row is flagged factur-x-detected; use Factur-X Toolkit to validate or extract it.

Zapier: my Zap times out. Zapier's synchronous "Run Actor" action stops waiting after 30 seconds. For batches, run the Actor asynchronously and start the next step from Apify's "Finished Actor run" trigger, then read the results with the "Fetch dataset items" search.

Limits

  • Layout comes from pdfplumber (text positions, font sizes, tables). Headings are inferred from font size and from numbered section titles (2. Introduction, 3.1 Profiles), so on some PDFs the heading path is approximate. Multi-column magazines and sparse banner tables can come out imperfect.
  • OCR is local Tesseract, not a commercial engine. Confidence is the mean word confidence, not a calibrated probability.
  • Animated GIF and WebP: the first frame only. Encrypted PDFs are refused (a PDF with only an owner password opens).
  • Default memory is 1024 MB. An 80-page text PDF peaked at 159 MB in testing. Raise it toward 4096 MB only for long OCR batches.
  • After a platform migration, the run resumes from the STATE record and does not bill a document twice.

Open-source components

Built on pdfplumber and pdfminer.six (MIT), pypdfium2/PDFium (Apache-2.0 and BSD-3-Clause), pypdf (BSD-3-Clause), python-docx, openpyxl and python-pptx (MIT), BeautifulSoup and markdownify (MIT), Pillow (MIT-CMU), and Tesseract OCR (Apache-2.0). None is AGPL or GPL.

Feedback

Found a document that converts badly? Open an issue on the Actor's Issues tab with a public link to the file. That's the fastest way to get it fixed.