Office Docs to Markdown: DOCX, PPTX, XLSX, ODT & EPUB
Pricing
$2.50 / 1,000 document converteds
Office Docs to Markdown: DOCX, PPTX, XLSX, ODT & EPUB
Convert DOCX, PPTX, XLSX, ODT/ODS/ODP and EPUB files to Markdown or text with tables, metadata and RAG chunks. $2.50 per 1,000 documents; failed files are free.
Pricing
$2.50 / 1,000 document converteds
Rating
0.0
(0)
Developer
Kaizoku Data
Maintained by CommunityActor stats
0
Bookmarked
2
Total users
1
Monthly active users
10 days ago
Last modified
Categories
Security
Limited permissionsShare
Convert Word, PowerPoint, Excel, OpenDocument and EPUB files to Markdown or plain text in bulk. Give it file links, uploads or a key-value store, and get one row per document: headings, lists, bold/italic, links and tables as Markdown, page/slide/sheet/chapter markers, metadata and optional RAG chunks with page numbers. Use it to feed documents into search, LLM prompts or a vector database. No browser, no LibreOffice, no AI service: plain code that reads the files.
Price: $2.50 per 1,000 documents ($0.0025 per document), per document, not per page, the same as PDF to Text & Markdown. Only documents that convert are charged; empty, encrypted and failed files are free. No start fee on top of Apify's default.
- Seven formats, one output shape: DOCX, PPTX, XLSX, ODT, ODS, ODP and EPUB (also DOCM, XLSM, PPTM, PPSX and templates). The format is read from the file's bytes, not its name.
- Structure kept: headings from Word styles and outline levels, bulleted and numbered lists, tables, links, footnotes, slide titles, sheet names, optional speaker notes.
- RAG-ready: chunks of about N characters with
pageStart/pageEnd(page, slide, sheet or chapter numbers), the same shape as PDF to Text & Markdown, so PDFs and Office files can share one pipeline. - Safe with hostile files: zip-bomb caps (size, ratio, entry count), no XML entity expansion or external entities, macros never run, embedded files never opened, each document converted in an isolated worker with time and memory limits.
- Quick start: press Start. The prefilled input converts a 2-page sample DOCX and a 3-sheet sample XLSX (Apache POI test files) in a few seconds for $0.005.
What it does
- Reads the files you list: direct URLs, uploads from your computer, or records of one of your key-value stores. It does not crawl websites or look for documents.
- Converts each file in an isolated worker into the
markdownfield (ortext, if you choose plain text), with a marker per page, slide, sheet or chapter and---between them, like PDF to Text. - Reports what was converted: format, page/slide/sheet/chapter count, words, tables, pictures, which caps cut the document, sheet sizes, and document properties (title, subject, description, keywords, language, dates, application). Author, last-modified-by, company and manager names are never returned.
What each format gives:
| Format | Unit | What is kept |
|---|---|---|
| DOCX, DOCM, DOTX | page | Headings (styles "Heading 1–9", "Title", outline levels), paragraphs, bold/italic/strike, links, bulleted and numbered lists, tables (merged cells as empty cells), footnotes and endnotes, text boxes. Tracked changes read as accepted (deleted text left out). Hidden text, comments, headers, footers and field codes are left out. |
| PPTX, PPTM, PPSX, POTX | slide | Slide title as a heading, bullet lists, text boxes, tables, optional speaker notes. Slide-number, date and footer placeholders are left out. |
| XLSX, XLSM, XLTX | sheet | One Markdown table per sheet (first non-empty row as header), saved formula results, ISO dates, TRUE/FALSE. Empty rows dropped, numbers as saved (no number formatting). |
| ODT | page | Headings, paragraphs, lists, tables, links, bold/italic, footnotes. Comments and tracked deletions left out. |
| ODS | sheet | Like XLSX. Huge "repeated" empty rows and columns cost nothing. |
| ODP | slide | Title, outline lists, text frames, tables, optional notes. |
| EPUB 2 and 3 | chapter | Chapters in reading order: headings, paragraphs, lists, quotes, code blocks, tables, links. Scripts and styles skipped. |
DOCX and ODT pages come from the page breaks saved in the file: Word and LibreOffice save where each page ended the last time they laid out the document, plus manual page breaks. A file saved without them counts as one page.
Input
| Field | What it does |
|---|---|
File URLs (files) | Direct http(s) links, one per line. Default (prefill): two small public sample files. |
Uploaded files (uploadedFiles) | Upload from your computer (the Console stores the file in a key-value store and passes its record URL), or paste record URLs. |
Key-value store (keyValueStoreId) | Pick one of your stores. The Actor gets read access to that store only (works with Limited permissions). Every record whose key ends in a supported extension is converted, unless you list keys. |
Record keys (keyValueStoreKeys) | Only these records of the chosen store. |
Output format (outputFormat) | markdown (default) or text (plain text; tables as tab-separated lines). |
Split into chunks for RAG (chunkForRag) | Off by default. On: a chunks array of about Chunk size (chunkSize, default 1,000) characters with Chunk overlap (overlap, default 100), cut at paragraph, line, sentence or word boundaries. chunkOverlap is accepted too. |
Include tables (includeTables) | On by default. Off: tables in documents, slides and e-books are left out. Spreadsheet sheets are always kept. |
Include speaker notes (includeNotes) | Off by default. On: notes under each slide (PPTX, ODP). |
| Max pages / slides / sheets | maxPages (DOCX/ODT pages, EPUB chapters; default 200, max 2,000), maxSlides (default 200, max 1,000), maxSheets (default 20, max 200). Cut documents are marked truncated. |
Max rows per sheet (maxRows) | Default 10,000, max 50,000. The rest of the sheet is not read. |
Max file size (MB) (maxFileSizeMb) | Default 50, max 100. Larger files are free error rows. The run memory also sets a limit (61 MB at 512 MB with 2 parallel files, 92 MB with 1; 100 MB from 1,024 MB); the log says when it applies. |
Download timeout (downloadTimeoutSecs) | Per file, default 60 s. Waiting for a site's Crawl-delay is not counted. |
Parallel files (concurrency) | Downloads at once, 1 to 4 (default 2), at most 2 per host. |
Examples (copy-paste inputs)
1. Knowledge base (RAG): Word manuals and slide decks to chunks with page and slide numbers
{"files": ["https://example.org/handbook.docx", "https://example.org/training.pptx"],"chunkForRag": true,"chunkSize": 1000,"overlap": 150,"includeNotes": true}
2. Spreadsheets to Markdown tables for an LLM prompt: first 2 sheets, at most 200 rows each
{"files": ["https://example.org/budget-2025.xlsx", "https://example.org/inventory.ods"],"maxSheets": 2,"maxRows": 200}
3. Your own files from a key-value store, as plain text
{"keyValueStoreId": "YOUR_STORE_ID","keyValueStoreKeys": ["contract-2025-01.docx", "minutes.odt"],"outputFormat": "text"}
Using your own files
- Uploaded files: the field's upload button stores the file in a key-value store of your account and hands the Actor its record URL. The run reads
api.apify.comkey-value-store record URLs with its own token, but only on the first request to that URL (never on a redirect, never to another host). With Limited permissions the run may not be able to read every store: if an upload fails withhttp401/403, use the Key-value store picker instead. - Key-value store: the safest way for private files. Pick the store; the run gets read access to it only. Name records with their extension (
report.docx) or list their keys. - For other private files use a signed or temporary link.
Output
One dataset item per file, in completion order (inputIndex is the position in your input):
{"inputIndex": 0,"url": "https://example.org/report.docx","source": "url","fileName": "report.docx","status": "ok","message": null,"format": "docx","unit": "page","pageCount": 2,"pagesProcessed": 2,"truncated": false,"truncatedBy": [],"tableCount": 1,"imageCount": 0,"charCount": 485,"wordCount": 85,"markdown": "<!-- page 1 -->\n\n# Quarterly Report\n\nRevenue grew **12 percent** and costs were *flat*.[^1]\n\n- Hiring in two regions\n- New office lease\n\n| Region | Q1 | Q2 |\n| --- | --- | --- |\n| North | 120 | 135 |\n\n---\n\n<!-- page 2 -->\n\n## Outlook\n\n...\n\n[^1]: Source: internal survey, 2025.","metadata": { "title": "Quarterly Report", "subject": null, "description": null, "keywords": null, "language": null, "creationDate": "2025-03-01T10:00:00.000Z", "modDate": "2025-03-02T11:00:00.000Z", "application": "Microsoft Office Word", "hasMacros": false, "embeddedObjects": 0 },"fileSizeBytes": 4088,"processedAt": "2026-10-27T10:00:00.000Z"}
With chunks on, each row also has chunks: [{ index, text, charStart, charEnd, pageStart, pageEnd }] (offsets into markdown or text). Spreadsheet rows also have sheets: [{ index, name, hidden, rows, columns, rowsTruncated, dimension }].
Status and error codes (only ok is charged)
status / errorCode | Meaning | Charged |
|---|---|---|
ok | Converted (also when cut by a cap: see truncated) | Yes, $0.0025 |
empty | No text found (only pictures, charts or empty sheets) | No |
encrypted | Password-protected Office or OpenDocument file, or DRM-protected EPUB | No |
unsupported-format | Not one of the supported formats: old binary .doc/.xls/.ppt, .xlsb, PDF (use PDF to Text), RTF, other archives | No |
not-a-document | The link returned a web page, or the record is JSON | No |
damaged | The file is broken or its main part is missing | No |
zip-bomb | Too many parts, too large when unpacked, or compressed suspiciously well | No |
too-complex | Nested too deeply, needs more memory than allowed, or used up its work budget before any content | No |
too-large | File or part above the size limits | No |
timeout | Download or conversion took too long | No |
row-too-large | The converted row would exceed the 9 MB dataset item limit | No |
http, refused | The server answered an error, or refused (401, 403, 407, 429, 451); refusals are never retried, a host is skipped after 3 | No |
robots-blocked, robots-unreachable, slow-host, host-skipped | robots.txt disallows the file, could not be read, or asks for a Crawl-delay this run cannot afford; or the host refused earlier files | No |
platform, blocked, dns, network, invalid-url, redirects | Video/social/Google link (e.g. Google Docs or Drive), private address, DNS or network failure, bad URL, too many redirects | No |
not-found, kv-read | No record with that key, or the store could not be read | No |
truncated: true with truncatedBy (maxPages, maxSlides, maxSheets, maxRows, size or time) means the document was converted only up to that cap; it is still ok and charged once.
The run summary (key-value store record RUN_SUMMARY) has counts per status and error code, documents charged, bytes read, converter time and why the run stopped.
Pricing
Pay per event: $0.0025 per document with status ok (event document-converted), plus Apify's standard Actor-start event. There is no additional start fee from this Actor. The price is per document, not per page, slide, sheet or row.
| Documents converted | Cost |
|---|---|
| 10 | $0.025 |
| 100 | $0.25 |
| 1,000 | $2.50 |
| 10,000 | $25 |
Add Apify's Actor-start event of $0.00005 per GB of run memory, once per run ($0.000025 at the default 512 MB). Empty, encrypted and failed files cost nothing. Set Max total charge in the run options to cap spending: the run never fetches a document it could not pay for, and stops cleanly (stoppedBy: "max-charge").
Limits
- 1,000 files per run (URLs, uploads and store records together). Files up to 100 MB (default 50 MB). At 512 MB of run memory the largest file is 61 MB with 2 parallel files or 92 MB with 1 (the file bytes and the converter must fit the run's memory); use 1,024 MB or more for 100 MB files.
- Output per document: about 3 million characters (1.5 million with chunks), so a row fits the 9 MB dataset item limit; longer documents are cut and marked
truncated(size). - Work budget per document: each document may use about 2.25 CPU-seconds (about 18 s of wall time per converter worker at 512 MB and 1 GB); after that it is returned as far as it was read and marked
truncated(time) and is charged like any other cut document (truncatedBysays why). A conversion still running at 2.5 times that budget plus 10 s is stopped (freetimeoutrow). Normal documents use a fraction of it (a 50-page DOCX about 1.2 s at 512 MB). - Sheets: at most 256 columns and 32,767 characters per cell.
- ZIP safety caps: at most 10,000 parts, 1 GB declared unpacked size, 300 MB actually unpacked per document, 120 MB per part, parts above 8 MB compressed more than 400:1 are refused. Nested archives and embedded files are never opened.
- Not supported: old binary
.doc,.xls,.ppt,.xlsb, PDF (use PDF to Text & Markdown), RTF, scanned pages or pictures (no OCR), charts and SmartArt, formulas (their saved results are shown), number formatting, password-protected or DRM files. - Free-work caps: the run stops (
stoppedBy: "unpaid-work-cap") after 50 free rows that outnumber the converted ones, 500 MB read for free rows, or 600 s of converter time on free rows, so a list of bad links cannot run for long. - Memory: default and minimum 512 MB (one converter worker); 1,024 MB uses two workers and halves the wall time of a batch. A document that needs more memory than its worker has is a free
too-complexrow; run with more memory for very large spreadsheets. The price does not change with memory.
Use it via API, n8n or Make
Get an API token in Apify Console → Settings → API & Integrations, and keep it in an environment variable (APIFY_TOKEN below). Never paste it into shared code.
cURL (starts a run, waits for it and returns one row per file; this synchronous endpoint waits up to 5 minutes):
curl -X POST "https://api.apify.com/v2/acts/ataism~docs-to-markdown/run-sync-get-dataset-items?token=$APIFY_TOKEN" \-H "Content-Type: application/json" \-d '{"files": ["https://raw.githubusercontent.com/apache/poi/5de26e846918e4fbbeadce96ecdef0c92548a8b9/test-data/document/SampleDoc.docx"], "chunkForRag": true, "chunkSize": 1000}'
JavaScript (npm install apify-client):
import { ApifyClient } from 'apify-client';const client = new ApifyClient({ token: process.env.APIFY_TOKEN });const run = await client.actor('ataism/docs-to-markdown').call({files: ['https://example.org/handbook.docx', 'https://example.org/prices.xlsx'],chunkForRag: true,chunkSize: 1000,overlap: 150,});const { items } = await client.dataset(run.defaultDatasetId).listItems();for (const doc of items) console.log(doc.fileName, doc.status, doc.format, doc.pageCount, doc.chunks?.length ?? 0);
Python (pip install "apify-client>=3"):
import osfrom apify_client import ApifyClientclient = ApifyClient(os.environ["APIFY_TOKEN"])run = client.actor("ataism/docs-to-markdown").call(run_input={"files": ["https://example.org/handbook.docx", "https://example.org/deck.pptx"],"chunkForRag": True,"chunkSize": 1000,})for doc in client.dataset(run.default_dataset_id).iterate_items():if doc["status"] != "ok":print("skipped (not charged):", doc["url"], doc["status"], doc["message"])continuefor chunk in doc.get("chunks", []):print(doc["fileName"], doc["unit"], chunk["pageStart"], chunk["text"][:80])
(With apify-client versions before 3.0, call() returns a dict: use run["defaultDatasetId"].)
n8n:
- Start with any trigger that gives you file links (a form, a sheet row, an email with a link).
- Add the Apify node, operation Run an Actor and get dataset, Actor
ataism/docs-to-markdown, input{"files": ["{{ $json.fileUrl }}"], "chunkForRag": true}. - Use
markdownin the next node (an AI summary, a Notion page) or split outchunksinto your vector store node.
Make:
- Add the Apify module Run an Actor, choose
ataism/docs-to-markdown, put your file links in the input JSON and set Run synchronously to Yes. - Add the Apify module Get Dataset Items and map the run's default dataset ID (one bundle per file).
- Send
markdown,textorchunksto your next module (OpenAI, Google Docs, Airtable, a webhook).
Privacy: transient processing
- Files are read into memory, converted in an isolated worker, and discarded. The Actor stores nothing except the dataset items you asked for, the run summary and a progress record with counts (used to resume after a platform restart). No copy of a file is saved and nothing is kept between runs.
- Document contents are never written to the log. Logs show only the host and path of a URL (not its query string), or the record key, and the status.
- Documents can contain personal data (names, contact details, figures). This Actor outputs what is in your files: the text is not filtered or redacted. You decide which files to convert and you are responsible for having the right to process them (copyright, licences, confidentiality, data-protection law).
- Metadata is limited to title, subject, description, keywords, language, dates, the saving application and the macro/embedded-file flags. Author, last-modified-by, company and manager names are not returned; comments (which carry author names) and tracked deletions are left out.
- Your dataset belongs to your Apify account and follows your retention settings. The
urlfield echoes what you supplied (including a signed query string if you used one), so treat the dataset as sensitive if your URLs are. - No data is sent to any AI or third-party service. The conversion is plain code running inside the Actor.
Safety and fair use of other sites
- Only
httpandhttpsURLs to public addresses are fetched. Private, loopback, link-local and internal addresses and other schemes are blocked, including after redirects (at most 5, each re-checked) and at connection time. Links to video, social and Google services (including Google Docs and Drive) are refused before any request (platform): download the file and upload it instead. - The User-Agent is
docs-to-markdown-actor/0.1 (https://apify.com/ataism/docs-to-markdown). robots.txt of each host is checked (exact product-token match, else*); a disallowed file is not downloaded. ACrawl-delayis honoured in full and never shortened; a host that asks for more than this run can afford to wait (1.8 s at 1 GB, 3.6 s at 512 MB; 600 s in total per run) is skipped as freeslow-hostrows. At most 2 downloads at a time per host, at least 250 ms apart. No cookies, no proxies, no CAPTCHA solving; blocks are reported, not worked around. - Documents are parsed as data. Macros (VBA projects) are never run, embedded objects and nested archives are never opened, external references and XML entities are never resolved.
FAQ
Does it work with old .doc or .xls files? No, only the XML-based formats (DOCX, XLSX, PPTX and OpenDocument). Save old files in the new format first. They are reported as unsupported-format and not charged.
Can it read PDFs? No. Use PDF to Text & Markdown: it returns the same row shape, chunks included.
Is it charged per page? No, per document. A 300-page report and a 1-page memo cost the same $0.0025 (pages above maxPages, default 200, are not converted).
Why is pageCount different from what Word shows? DOCX/ODT pages come from the page breaks the file saved when it was last laid out. Files written by other programs often have none and count as one page; the text is complete either way.
Are formulas or charts converted? Formulas show their last saved result. Charts, pictures and SmartArt are not converted (pictures are counted in imageCount).
Can it find documents on a website? No. It only reads the URLs, uploads and store records you give it.
Open-source components
Conversion is our own code on top of htmlparser2 12.0.0 (MIT; streaming XML/HTML tokenizer, with its dependencies entities 8.1.0, domhandler 6.0.1, domutils 4.0.2 and domelementtype 3.0.0, BSD-2-Clause, and dom-serializer 3.1.1, MIT) and yauzl 3.4.0 (MIT; ZIP reader, with pend 1.2.0, MIT), plus the Apify SDK (apify 3.7.2, Apache-2.0). No LibreOffice, no browser and no GPL/AGPL code. The sample files are Apache POI test data (Apache-2.0).