# PDF to Markdown for RAG & LLMs: OCR, DOCX, Excel (`rigel/doc-to-markdown`) Actor

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.

- **URL**: https://apify.com/rigel/doc-to-markdown.md
- **Developed by:** [Rigel](https://apify.com/rigel) (community)
- **Categories:** AI, Developer tools, Automation
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $2.00 / 1,000 documents

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.

Learn more: https://docs.apify.com/actors/running/actors-in-store.md#pay-per-event

## What's an Apify Actor?

An Actor is a serverless cloud program that runs on the Apify platform. It has two run modes.
In Batch mode, an Actor accepts a well-defined JSON input, performs an action which can take anything from a few seconds to a few hours,
and optionally produces a well-defined JSON output, datasets with results, or files in key-value store.
In Standby mode, an Actor provides a web server which can be used as a website, API, or an MCP server.

Apify vocabulary and the platform model are defined once, in the agent quickstart at https://apify.com/agents.md.

## How to integrate an Actor?

If asked about integration, you help developers integrate Actors into their projects.
You adapt to their stack and deliver integrations that are safe, well-documented, and production-ready.

Do not guess an integration path. Every one of them is in the agent quickstart at https://apify.com/agents.md: the Apify MCP server, Agent Skills with the Apify CLI, the JavaScript and Python clients, the REST API, and the account-free path for an agent with no human to sign in. It also carries the rule on stating cost before the first paid run.

For examples already wired to this Actor's own input schema, see the [API](#api) section below.

Each client library has reference documentation the quickstart does not restate: [JavaScript/TypeScript](https://docs.apify.com/api/client/js/docs.md) (`npm install apify-client`) and [Python](https://docs.apify.com/api/client/python/docs.md) (`pip install apify-client`).

# README

### 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:

| Event | Price | When |
| --- | --- | --- |
| Document | $0.002 | A file that produced at least one billed page |
| Text page | $0.0003 | A 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.004 | A PDF page or image frame read with Tesseract. Replaces the text-page event for that page |
| Free | $0 | Download failure, unsupported type, encrypted file, empty result, duplicate in the same run, pages skipped by `pages` |

Real examples:

| Input | Charged | Cost |
| --- | --- | --- |
| IRS Publication 509 (PDF, 14 text pages) | 1 document + 14 pages | $0.0062 |
| NIST CSF 2.0, pages 1–8, as RAG chunks | 1 document + 8 pages | $0.0044 |
| Word file under 3,000 characters | 1 document + 1 page | $0.0023 |
| Excel workbook, 2 sheets | 1 document + 2 pages | $0.0026 |
| One scanned page | 1 document + 1 OCR page | $0.006 |
| 20-page text PDF / 20-page scan | | $0.008 / $0.082 |
| 1,000 ten-page text PDFs | 1,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](https://apify.com/rigel/doc-to-markdown/examples/pdf-to-markdown-rag-chunks) · [PDF to Markdown for LLMs](https://apify.com/rigel/doc-to-markdown/examples/pdf-to-markdown-for-llm) · [Scanned PDF OCR](https://apify.com/rigel/doc-to-markdown/examples/scanned-pdf-ocr-to-markdown) · [Image OCR](https://apify.com/rigel/doc-to-markdown/examples/image-ocr-to-text) · [DOCX to Markdown](https://apify.com/rigel/doc-to-markdown/examples/docx-to-markdown) · [Excel tables](https://apify.com/rigel/doc-to-markdown/examples/excel-xlsx-tables-to-markdown-csv) · [PPTX with notes](https://apify.com/rigel/doc-to-markdown/examples/pptx-to-text-with-notes) · [Batch PDF URLs](https://apify.com/rigel/doc-to-markdown/examples/batch-pdf-urls-to-markdown)

### Output

One dataset row per document:

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

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

| `errorCode` | Meaning |
| --- | --- |
| `DOWNLOAD` | HTTP error, DNS failure or the 60-second download timeout |
| `FILE_ACCESS_DENIED` | The key-value record refused the run. Pass the signed `recordPublicUrl` (`?signature=`), or use Upload files in the Console |
| `SSRF` | Private, loopback or link-local address (also checked after redirects) |
| `UNSUPPORTED` | File type not handled, including legacy `.doc` and `.ppt` |
| `ENCRYPTED` | Password-protected PDF |
| `EMPTY` | No text found |
| `CORRUPT` | The file could not be parsed |
| `CONTENT_MISMATCH` | An HTML page saved under a `.pdf` or `.docx` name |
| `TOO_LARGE` / `ZIP_BOMB` | Over `maxFileMb`, or an Office file that unpacks to more than 200 MB |
| `TIMEOUT` | The conversion did not finish within `timeoutSecs` |
| `OCR_FAILED` | Tesseract could not read the page |
| `PAGE_RANGE` | `pages` is invalid or selects no page of this document |
| `LIMIT` | The spending cap was reached before this document |
| `INPUT` / `INTERNAL` | Invalid source item / unexpected error |

### Input

| Field | Default | Role |
| --- | --- | --- |
| `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 |
| `output` | `markdown` | `markdown`, `json`, `text` or `chunks` |
| `ocr` | `auto` | `auto` 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. |
| `ocrLanguages` | `eng`, `fra` | Also `deu`, `spa`, `ita`, `por`, `nld` |
| `maxOcrPages` | 40 | OCR pages billed per document. Further scanned pages are skipped, free, and flagged `truncated-ocr` |
| `ocrDpi` | 200 | 120–300 |
| `pages` | all | Page range for PDF, PowerPoint or TIFF, e.g. `1-5,8` |
| `tables`, `tableCsv` | true, false | Markdown tables; optional ZIP of CSV files per document |
| `excelOutput` | `markdown` | XLSX/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`, `includeNotes` | true, true | `<!-- page: N -->` markers; PowerPoint speaker notes |
| `chunkSize`, `chunkOverlap` | 1500, 150 | Chunk 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 |
| `maxPagesPerDocument` | 500 | Beyond this, pages are skipped and flagged `truncated-pages` |
| `maxRowsPerSheet` | 20000 | Truncated sheets are flagged `truncated-rows` |
| `maxFileMb`, `maxDocuments` | 100, 50 | Up to 100 MB per file. Default 50 documents, maximum 1,000. A run stops at 3600 seconds |
| `timeoutSecs` | 300 | Per 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

```bash
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=…`

```python
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"),
)
```

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

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

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

### Related Actors

- [Website Screenshot API, Full Page & URL/HTML to PDF](https://apify.com/rigel/website-screenshot-pdf): turn web pages into PDFs first, then convert those PDFs to Markdown chunks here.
- [Factur-X Validator & Generator](https://apify.com/rigel/facturx-toolkit): when a PDF is a Factur-X / ZUGFeRD e-invoice (rows flagged `factur-x-detected`), validate it or extract its XML to JSON.

### 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](https://apify.com/rigel/facturx-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.

# Actor input Schema

## `sources` (type: `array`):

Direct http(s) links to files you are allowed to fetch. Each item is {"url": "..."}. A requestsFromUrl list of links is accepted. {"kvKey": "..."} is not valid here: the platform rejects it before the run because this list only accepts URLs. Put that object in the dataset selected by datasetId, and select the store with keyValueStoreId. Private and loopback addresses are refused.

## `files` (type: `array`):

One or more files from your computer. The Console stores each file and passes its signed record URL. From the API, pass that recordPublicUrl (https://api.apify.com/v2/key-value-stores/<store>/records/<fileName>?signature=…). The record key is the original file name. Document URLs in sources keep working.

## `datasetId` (type: `string`):

Optional dataset to read. Each item is one source: a url string, or an object with url, base64, kvKey, or requestsFromUrl. Under limited permissions the run can read only the dataset selected here.

## `keyValueStoreId` (type: `string`):

Key-value store read for dataset items {"kvKey": "report.pdf"}. Those items belong in the dataset selected by datasetId, not in sources. storeId on an item, when set, must be this same id. When storeId is omitted, this store is used. Under limited permissions the run can read only the store selected here.

## `output` (type: `string`):

markdown: one row with Markdown. json: Markdown plus per-page text. text: plain text. chunks: one row per RAG chunk with page numbers and heading path.

## `ocr` (type: `string`):

auto OCRs only pages that have no usable text layer. always OCRs every PDF page and is the only mode for images. never stays on the text layer.

## `ocrLanguages` (type: `array`):

Tesseract language codes.

## `maxOcrPages` (type: `integer`):

Cap on pages read with Tesseract in one document. Extra scanned pages are skipped, flagged truncated-ocr, and not charged. 40 fits the default 300 second limit.

## `ocrDpi` (type: `integer`):

Raster DPI for scanned PDF pages. 200 balances accuracy and compute. Large pages are scaled down automatically.

## `pages` (type: `string`):

PDF, PowerPoint or TIFF page range, for example 1-5,8. Omitted pages are not charged. Leave empty for every page.

## `tables` (type: `boolean`):

Write tables as Markdown. CSV files are separate and off unless tableCsv is true.

## `tableCsv` (type: `boolean`):

Also store one ZIP of CSV files per document in the key-value store. Off by default. Markdown tables are unchanged.

## `excelOutput` (type: `string`):

Spreadsheets only (XLSX and XLS). markdown keeps one document row with Markdown tables. rows writes one JSON item per data row (sheet, rowIndex, values by column). both keeps the Markdown row and adds the row items. A sheet is still one billed page. Other file types ignore this field.

## `includePageMarkers` (type: `boolean`):

Insert  so a citation can point at the source page.

## `includeNotes` (type: `boolean`):

Include speaker notes under each slide.

## `chunkSize` (type: `integer`):

Characters per chunk when output is chunks, including overlap. Long paragraphs are cut on a sentence or a word. A long table is cut between rows and each piece repeats the header. One table row longer than this stays whole.

## `chunkOverlap` (type: `integer`):

Characters repeated from the previous chunk. Capped at half of chunkSize.

## `maxPagesPerDocument` (type: `integer`):

Safety cap. 500 text pages finish inside the default 300 second timeout at 1024 MB. Extra pages are skipped, the row is partial, and they are not charged.

## `maxRowsPerSheet` (type: `integer`):

Spreadsheet row cap. A truncated sheet is flagged truncated-rows and still charged as one sheet.

## `maxFileMb` (type: `integer`):

Files above this size are refused and not charged. 100 MB is the hard maximum.

## `maxDocuments` (type: `integer`):

How many URLs or files to process. 50 fits the 3600 second run limit for typical documents. Documents already finished are kept and charged if the run stops; the rest are not. Maximum 1000.

## `timeoutSecs` (type: `integer`):

Stop reading this document after this many seconds. Pages already converted are kept and charged; the rest are not. 300 seconds covers the default 500 text pages and 40 OCR pages at 1024 MB.

## `headers` (type: `object`):

Optional headers for YOUR files (for example Authorization). They are not logged. They are dropped if a redirect changes host.

## Actor input object example

```json
{
  "sources": [
    {
      "url": "https://www.irs.gov/pub/irs-pdf/p509.pdf"
    }
  ],
  "output": "markdown",
  "ocr": "auto",
  "ocrLanguages": [
    "eng",
    "fra"
  ],
  "maxOcrPages": 40,
  "ocrDpi": 200,
  "tables": true,
  "tableCsv": false,
  "excelOutput": "markdown",
  "includePageMarkers": true,
  "includeNotes": true,
  "chunkSize": 1500,
  "chunkOverlap": 150,
  "maxPagesPerDocument": 500,
  "maxRowsPerSheet": 20000,
  "maxFileMb": 100,
  "maxDocuments": 50,
  "timeoutSecs": 300
}
```

# Actor output Schema

## `results` (type: `string`):

Default dataset. One row per document, one row per chunk when output is chunks, or one JSON item per spreadsheet data row when excelOutput is rows.

## `markdown` (type: `string`):

OUTPUT.md when the run converted a single document. Otherwise one Markdown file per document in the key-value store. A table ZIP is added only when tableCsv is true.

## `summary` (type: `string`):

Counts, error codes and the amount charged. No document text.

## `tables` (type: `string`):

One ZIP per document when tableCsv is true. Each table's csvName is the file inside that ZIP.

# API

You can run this Actor programmatically using our API. Below are code examples in JavaScript, Python, and CLI, as well as the OpenAPI specification and MCP server setup.

## JavaScript example

```javascript
import { ApifyClient } from 'apify-client';

// Initialize the ApifyClient with your Apify API token
// Replace the '<YOUR_API_TOKEN>' with your token
const client = new ApifyClient({
    token: '<YOUR_API_TOKEN>',
});

// Prepare Actor input
const input = {
    "sources": [
        {
            "url": "https://www.irs.gov/pub/irs-pdf/p509.pdf"
        }
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("rigel/doc-to-markdown").call(input);

// Fetch and print Actor results from the run's dataset (if any)
console.log('Results from dataset');
console.log(`💾 Check your data here: https://console.apify.com/storage/datasets/${run.defaultDatasetId}`);
const { items } = await client.dataset(run.defaultDatasetId).listItems();
items.forEach((item) => {
    console.dir(item);
});

// 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/js/docs

```

## Python example

```python
from apify_client import ApifyClient

# Initialize the ApifyClient with your Apify API token
# Replace '<YOUR_API_TOKEN>' with your token.
client = ApifyClient("<YOUR_API_TOKEN>")

# Prepare the Actor input
run_input = { "sources": [{ "url": "https://www.irs.gov/pub/irs-pdf/p509.pdf" }] }

# Run the Actor and wait for it to finish
run = client.actor("rigel/doc-to-markdown").call(run_input=run_input)

# Fetch and print Actor results from the run's dataset (if there are any)
print(f"💾 Check your data here: https://console.apify.com/storage/datasets/{run.default_dataset_id}")
for item in client.dataset(run.default_dataset_id).iterate_items():
    print(item)

# 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/python/docs/quick-start

```

## CLI example

```bash
echo '{
  "sources": [
    {
      "url": "https://www.irs.gov/pub/irs-pdf/p509.pdf"
    }
  ]
}' |
apify call rigel/doc-to-markdown --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,rigel/doc-to-markdown"
        }
    }
}
```

The hosted server signs you in with OAuth on first connect, so no API token belongs in this config. Clients without OAuth support can send an `Authorization: Bearer <APIFY_API_TOKEN>` header instead, using a token from API & Integrations in Apify Console (https://console.apify.com/settings/integrations).

## OpenAPI specification

Download the OpenAPI definition: https://api.apify.com/v2/actors/edLj4uuDdssZO5FoR/builds/pRXu3A2MOcGs2r09Z/openapi.json
