# Document to Markdown – DOCX, PPTX, XLSX (`sfoln/document-to-markdown`)

Convert DOCX, PPTX, XLSX, XLS, ODS, CSV, EPUB, RTF, ODT, HTML, TXT and PDF files to clean Markdown with GFM tables, slides with speaker notes, sheets as tables or JSON rows. $3 per 1,000 documents (up to 20 pages or 10,000 rows each).

- Family: Documents & media
- Categories: AI, Developer tools, Automation
- Store: https://apify.com/sfoln/document-to-markdown (Markdown: https://apify.com/sfoln/document-to-markdown.md)
- JSON: https://www.sfoln.com/actors/document-to-markdown.json
- MCP: https://mcp.apify.com?tools=sfoln/document-to-markdown
- REST (sync): POST https://api.apify.com/v2/actors/sfoln~document-to-markdown/run-sync-get-dataset-items
- REST (async): POST https://api.apify.com/v2/actors/sfoln~document-to-markdown/runs

Document to Markdown converts office documents, spreadsheets, e-books and PDFs into clean Markdown for LLMs, RAG and search: Word headings, lists and tables become GFM, PowerPoint decks become one section per slide with speaker notes, Excel and OpenDocument sheets become Markdown tables (and, if you want, typed JSON rows), EPUBs become chapters. Give it document URLs, an uploaded file, data: URIs or Apify key-value store records; each document becomes one dataset record. The format is detected from the file itself, so wrong extensions do not matter.

## Price per delivered record

| Event | Price | Per 1,000 | Charged for |
| --- | --- | --- | --- |
| Converted document (`document`) | $0.003 | $3 / 1,000 | One document converted to Markdown, up to 20 pages (PDF pages, slides or 3,000-character text pages) or 10,000 spreadsheet rows. |
| Overage block (20 pages or 10k rows) (`document-overage`) | $0.0007 | $0.70 / 1,000 | Each further block of 20 pages or 10,000 spreadsheet rows of a converted document. |

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 10 per run.

## Run

```bash
curl -X POST "https://api.apify.com/v2/actors/sfoln~document-to-markdown/run-sync-get-dataset-items" \
  -H "Authorization: Bearer $APIFY_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"documentUrls":["https://raw.githubusercontent.com/mwilliamson/mammoth.js/791a189230977143cde64dbf9f1d24dce96ac8a1/test/test-data/tables.docx"]}'
```

MCP client configuration:

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

Example prompt: Convert https://raw.githubusercontent.com/mwilliamson/mammoth.js/791a189230977143cde64dbf9f1d24dce96ac8a1/test/test-data/tables.docx to Markdown with document-to-markdown and show me the table it contains.

## Input

The documents to convert: URLs, an uploaded file, data: URIs or key-value store records. Each document produces one dataset record with its Markdown, metadata and per-part sections.

| Field | Type | Default | Meaning |
| --- | --- | --- | --- |
| `documentUrls` | array | — | Document URLs: Documents to convert, one per line: public http(s) URLs, data: URIs (base64) or Apify key-value store records (kvs://<storeId>/<key>, or the record's API URL, read with the run's token). Formats: DOCX, PPTX, XLSX, XLS, ODS, CSV, EPUB, RTF, ODT, HTML, TXT and PDF, detected from the bytes (not the file name). Max 25 MB per office file, 50 MB per PDF. Accounts on the Apify free plan can process up to 10 documents per run (the first 20 pages or 10,000 rows of each). |
| `documentFile` | string | — | Or upload a document: A document from your computer (or its URL). It is converted together with the document URLs above. |
| `includeSections` | boolean | `true` | Per-part sections: Return sections[]: one entry per PDF page, slide (with speaker notes), sheet or e-book chapter, each with its own Markdown. |
| `includeRows` | boolean | `false` | Spreadsheet rows as JSON: For XLSX, XLS, ODS and CSV, also return every sheet's rows as JSON objects keyed by the header row (typed numbers, booleans and ISO dates) in tables[].rows. |
| `includePlainText` | boolean | `false` | Include plain text: Also return the content as plain text (text), without Markdown syntax. |
| `password` | string | — | PDF password: Password for encrypted PDFs (used for every PDF of this run). It is never written to the output. |
| `maxPages` | integer | `1000` | Max pages per document: Convert at most this many pages per document: PDF pages, slides, or pages of 3,000 characters for text documents. Every 20 pages beyond the first 20 are one document-overage event. |
| `maxRows` | integer | `100000` | Max spreadsheet rows: Convert at most this many non-empty rows per spreadsheet (all sheets together). Every 10,000 rows beyond the first 10,000 are one document-overage event. About 100,000 rows of 10 columns fit in the default 1 GB of memory; give the run 2 GB or more for bigger workbooks. |
| `maxSheets` | integer | `50` | Max sheets per workbook: Convert at most this many sheets of a workbook (in workbook order). |
| `maxItems` | integer | `1000` | Max documents: Safety cap on how many documents are processed in this run; further ones are ignored and counted as inputItemsDropped in the OUTPUT record. |
| `concurrency` | integer | `2` | Concurrency: Documents converted in parallel (1–5). |

Example input:

```json
{
  "documentUrls": [
    "https://raw.githubusercontent.com/mwilliamson/mammoth.js/791a189230977143cde64dbf9f1d24dce96ac8a1/test/test-data/tables.docx"
  ]
}
```

## 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 | object | Input: The document reference as submitted (data: URIs are shown as a short label). |
| `status` | string | Status: ok \| not_found \| error \| skipped_invalid \| skipped_budget \| skipped_free_plan \| skipped_unmetered |
| `charged` | boolean | Charged: Whether the record's document event was charged. |
| `chargedEvent` | string | Charged event: Pay-per-event event charged for this record (present only when charged). |
| `chargedCount` | integer | Events charged: Events actually charged when the charge came back short (present only then). |
| `source` | string | Source: document-converter: converted in the Actor. |
| `fetchedAt` | string | Fetched at: ISO timestamp. |
| `error` | object | Error: { code, message, status? } when status is not ok. Codes: invalid_reference, document_too_large, document_not_found, unsupported_document, not_readable, encrypted_document, wrong_password, corrupt_document, zip_bomb, timeout, storage_failed (converted, but the key-value store did not save the full content: see truncation), fetch errors (blocked_url, dns_not_found, network, http_error, …) and the runtime codes (budget_exhausted, free_plan_limit, …). |
| `warnings` | array | Warnings: Notes: limits, budget, scanned PDF pages, content moved to the key-value store. |
| `url` | string | null | URL: URL the document was downloaded from (after redirects); the record URL for key-value store records; null for data: URIs. |
| `fileName` | string | null | File name |
| `fileSizeBytes` | integer | null | File size (bytes) |
| `sha256` | string | null | SHA-256: Hash of the file bytes. |
| `format` | string | null | Format: docx \| pptx \| xlsx \| xls \| ods \| csv \| epub \| rtf \| odt \| html \| txt \| pdf (detected from the bytes). |
| `title` | string | null | Title: Document title from the metadata, else the first heading or slide title. |
| `metadata` | object | null | Metadata: { title, author, subject, description, keywords[], language, publisher, createdAt, modifiedAt } |
| `language` | string | null | Language: Detected language of the text (ISO 639-1; null for spreadsheets). |
| `pageCount` | integer | null | Pages: PDF pages, slides, or 3,000-character pages of text (0 for spreadsheets). |
| `rowCount` | integer | null | Rows: Non-empty spreadsheet rows converted (all sheets). |
| `wordCount` | integer | null | Words |
| `charCount` | integer | null | Characters: Characters of plain text. |
| `markdown` | string | null | Markdown |
| `markdownUrl` | string | null | Full Markdown URL: Set when the content exceeded the dataset item size: the full Markdown is in the run's key-value store; markdown holds the beginning. |
| `text` | string | null | Plain text: With includePlainText. |
| `textUrl` | string | null | Full text URL |
| `sections` | array | null | Sections: [{ type: page \| slide \| sheet \| chapter, index, title, markdown, notes?, hidden?, rowCount?, columnCount?, needsOcr? }] (includeSections). |
| `tables` | array | null | Tables: Spreadsheets: [{ sheet, index, rowCount, columnCount, columns, headerRow, hidden, rows? }]; rows with includeRows. |
| `tablesUrl` | string | null | Tables URL: Set when the JSON rows exceeded the dataset item size: tables with rows are in the run's key-value store. |
| `scannedPages` | array | null | Scanned PDF pages: PDF pages without a usable text layer (OCR them with pdf-ocr-markdown). |
| `truncated` | boolean | null | Truncated: Something was left out because of a limit, or (storage_failed) because the key-value store did not save it (see truncation). |
| `truncation` | array | null | What was left out |
| `billedBlocks` | integer | null | Billed blocks: 1 + document-overage events: blocks of 20 pages or 10,000 rows. |
| `overageCharged` | integer | null | Overage charged: document-overage events charged for this document. |

Example record:

```json
{
  "input": "https://example.com/files/q1-sales.xlsx",
  "status": "ok",
  "charged": true,
  "chargedEvent": "document",
  "source": "document-converter",
  "fetchedAt": "2026-10-14T10:00:00.000Z",
  "url": "https://example.com/files/q1-sales.xlsx",
  "fileName": "q1-sales.xlsx",
  "fileSizeBytes": 8317,
  "sha256": "8b1a9953c4611296a827abf8c47804d7e6c49c6b6b0f6d9a8f5f1f4e2d3c4b5a",
  "format": "xlsx",
  "title": "Sales Workbook",
  "metadata": {
    "title": "Sales Workbook",
    "author": "Example Author",
    "subject": null,
    "description": null,
    "keywords": [],
    "language": null,
    "publisher": null,
    "createdAt": "2026-04-01T09:00:00.000Z",
    "modifiedAt": "2026-04-01T09:00:00.000Z"
  },
  "language": null,
  "pageCount": 0,
  "rowCount": 4,
  "wordCount": 29,
  "charCount": 140,
  "markdown": "## Sales\n\n| Region | Q1 | Q2 | Total | Updated |\n| --- | --- | --- | --- | --- |\n| North | 1240 | 1310 | 2550 | 2026-04-02 |\n| South | 980 | 1002 | 1982 | 2026-04-03 |\n| West | 1105 | 1190 | 2295 | 2026-04-04 |",
  "markdownUrl": null,
  "sections": [
    {
      "type": "sheet",
      "index": 1,
      "title": "Sales",
      "markdown": "## Sales\n\n| Region | Q1 | Q2 | Total | Updated |\n…",
      "hidden": false,
      "rowCount": 4,
      "columnCount": 5
    }
  ],
  "tables": [
    {
      "sheet": "Sales",
      "index": 1,
      "rowCount": 4,
      "columnCount": 5,
      "columns": [
        "Region",
        "Q1",
        "Q2",
        "Total",
        "Updated"
      ],
      "headerRow": true,
      "hidden": false,
      "rows": [
        {
          "Region": "North",
          "Q1": 1240,
          "Q2": 1310,
          "Total": 2550,
          "Updated": "2026-04-02"
        }
      ]
    }
  ],
  "tablesUrl": null,
  "truncated": false,
  "truncation": [],
  "billedBlocks": 1,
  "overageCharged": 0
}
```

## FAQ

**Which formats are supported?**

DOCX, PPTX, XLSX, XLSB, XLS, ODS, CSV/TSV, EPUB, RTF, ODT, HTML, plain text (JSON and XML are returned as code blocks) and PDF (its text layer). The format is detected from the file's bytes, so a wrong extension does not matter. Legacy .doc and .ppt files are not supported: save them as DOCX or PPTX.

**How are pages counted for pricing?**

PDF pages and slides count as pages; for text documents a page is 3,000 characters of output (about 500 words); spreadsheets count non-empty rows. One document event covers 20 pages or 10,000 rows, each further block of 20 pages or 10,000 rows is one document-overage event ($0.0007).

**Can I get spreadsheet data as JSON?**

Yes: set includeRows and every sheet's rows come back in tables[].rows as JSON objects keyed by the header row, with numbers, booleans and ISO dates typed.

**Is it safe to convert untrusted files?**

Archives are checked before they are unpacked (entry count and uncompressed size, so zip bombs are refused), XML entity declarations are refused and nothing in a document is executed. Images, macros and embedded objects are ignored.


## Related

- [PDF Text Extractor – PDF to Text & Markdown](https://www.sfoln.com/actors/pdf-text-extractor.md) (`sfoln/pdf-text-extractor`)
- [PDF OCR to Markdown Converter](https://www.sfoln.com/actors/pdf-ocr-markdown.md) (`sfoln/pdf-ocr-markdown`)
- [Document Parser – AI PDF & Image to JSON](https://www.sfoln.com/actors/document-to-json.md) (`sfoln/document-to-json`)
- [URL to Markdown & Article Extractor](https://www.sfoln.com/actors/url-to-markdown-summary.md) (`sfoln/url-to-markdown-summary`)

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