SFOLN

Catalogue / Documents & media

Document to Markdown – DOCX, PPTX, XLSX

sfoln/document-to-markdown · AI, Developer tools, Automation

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).

About

What it does

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

Price per delivered record

EventPricePer 1,000Charged for
Converted document
document
$0.003$3 / 1,000One 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,000Each further block of 20 pages or 10,000 spreadsheet rows of a converted document.

Apify free plan: up to 10 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

curl · synchronous run, rows in the response
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.json · hosted Apify MCP server
{
  "mcpServers": {
    "apify": {
      "type": "http",
      "url": "https://mcp.apify.com?tools=sfoln/document-to-markdown"
    }
  }
}
asynchronous run · for large batches
POST https://api.apify.com/v2/actors/sfoln~document-to-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. Example prompt for an agent: 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

Input fields

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.

FieldTypeDefaultMeaning
documentUrlsarray—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).
documentFilestring—Or upload a document
A document from your computer (or its URL). It is converted together with the document URLs above.
includeSectionsbooleantruePer-part sections
Return sections[]: one entry per PDF page, slide (with speaker notes), sheet or e-book chapter, each with its own Markdown.
includeRowsbooleanfalseSpreadsheet 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.
includePlainTextbooleanfalseInclude plain text
Also return the content as plain text (text), without Markdown syntax.
passwordstring—PDF password
Password for encrypted PDFs (used for every PDF of this run). It is never written to the output.
maxPagesinteger1000Max 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. Range 1–5000.
maxRowsinteger100000Max 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. Range 1–200000.
maxSheetsinteger50Max sheets per workbook
Convert at most this many sheets of a workbook (in workbook order). Range 1–200.
maxItemsinteger1000Max documents
Safety cap on how many documents are processed in this run; further ones are ignored and counted as inputItemsDropped in the OUTPUT record. Range 1–10000.
concurrencyinteger2Concurrency
Documents converted in parallel (1–5). Range 1–5.
Example input
{
  "documentUrls": [
    "https://raw.githubusercontent.com/mwilliamson/mammoth.js/791a189230977143cde64dbf9f1d24dce96ac8a1/test/test-data/tables.docx"
  ]
}

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
inputstring | objectInput
The document reference as submitted (data: URIs are shown as a short label).
statusstringStatus
ok | not_found | error | skipped_invalid | skipped_budget | skipped_free_plan | skipped_unmetered e.g. "ok"
chargedbooleanCharged
Whether the record's document event was charged.
chargedEventstringCharged event
Pay-per-event event charged for this record (present only when charged).
chargedCountintegerEvents charged
Events actually charged when the charge came back short (present only then).
sourcestringSource
document-converter: converted in the Actor.
fetchedAtstringFetched at
ISO timestamp.
errorobjectError
{ 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, …).
warningsarrayWarnings
Notes: limits, budget, scanned PDF pages, content moved to the key-value store.
urlstring | nullURL
URL the document was downloaded from (after redirects); the record URL for key-value store records; null for data: URIs.
fileNamestring | nullFile name
fileSizeBytesinteger | nullFile size (bytes)
sha256string | nullSHA-256
Hash of the file bytes.
formatstring | nullFormat
docx | pptx | xlsx | xls | ods | csv | epub | rtf | odt | html | txt | pdf (detected from the bytes).
titlestring | nullTitle
Document title from the metadata, else the first heading or slide title.
metadataobject | nullMetadata
{ title, author, subject, description, keywords[], language, publisher, createdAt, modifiedAt }
languagestring | nullLanguage
Detected language of the text (ISO 639-1; null for spreadsheets).
pageCountinteger | nullPages
PDF pages, slides, or 3,000-character pages of text (0 for spreadsheets).
rowCountinteger | nullRows
Non-empty spreadsheet rows converted (all sheets).
wordCountinteger | nullWords
charCountinteger | nullCharacters
Characters of plain text.
markdownstring | nullMarkdown
markdownUrlstring | nullFull 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.
textstring | nullPlain text
With includePlainText.
textUrlstring | nullFull text URL
sectionsarray | nullSections
[{ type: page | slide | sheet | chapter, index, title, markdown, notes?, hidden?, rowCount?, columnCount?, needsOcr? }] (includeSections).
tablesarray | nullTables
Spreadsheets: [{ sheet, index, rowCount, columnCount, columns, headerRow, hidden, rows? }]; rows with includeRows.
tablesUrlstring | nullTables URL
Set when the JSON rows exceeded the dataset item size: tables with rows are in the run's key-value store.
scannedPagesarray | nullScanned PDF pages
PDF pages without a usable text layer (OCR them with pdf-ocr-markdown).
truncatedboolean | nullTruncated
Something was left out because of a limit, or (storage_failed) because the key-value store did not save it (see truncation).
truncationarray | nullWhat was left out
billedBlocksinteger | nullBilled blocks
1 + document-overage events: blocks of 20 pages or 10,000 rows.
overageChargedinteger | nullOverage charged
document-overage events charged for this document.
Example record
{
  "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

Questions

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.

Machine-readable

This page for agents