PDF, DOCX & XLSX to Markdown Converter for RAG
Pricing
$3.00 / 1,000 document converteds
PDF, DOCX & XLSX to Markdown Converter for RAG
Turn PDF, DOCX, XLSX, HTML, CSV and text links into clean Markdown with headings, lists and tables, plus heading-based chunks with token counts for RAG. No browser, no OCR. Pay per document converted.
Pricing
$3.00 / 1,000 document converteds
Rating
0.0
(0)
Developer
Mohammed Samaan
Maintained by CommunityActor stats
0
Bookmarked
2
Total users
1
Monthly active users
4 days ago
Last modified
Categories
Share
Turn links to PDF, DOCX, XLSX, HTML, CSV and plain-text files into clean Markdown that keeps headings, lists and tables, then get ready-made chunks with the heading path and an approximate token count. Built for people who feed documents to an LLM, a vector database or a search index and do not want to write a parser for each file type.
Everything runs over plain HTTP inside the Actor. There is no browser, no proxy, no login and no OCR. You pay one small price for each document that was converted, and nothing for a link that failed.
What you get
- One dataset row per link, with the Markdown, chunks, word count, page count, title and a
warningslist. - PDF with a text layer: reading order rebuilt from text positions (two-column pages are read left column first), headings found from section numbers (1, 4.1, A.2), keywords (Abstract, References), small capitals, bold run-in lead-ins and font size, a wrapped title joined into one heading, author lines kept as text, bullet and numbered lists, simple tables, wrapped lines joined into paragraphs, repeated page headers and footers removed, and a
<!-- page N -->marker before each page. - DOCX: headings, bold and italic, lists (numbering continues across nested items) and tables (several paragraphs in one cell are separated with
<br>). - XLSX: one Markdown table per visible sheet, dates as ISO text. Formulas are never evaluated: a formula cell shows the value stored in the file.
- HTML: the main content of the page, without navigation, footers, cookie banners, scripts, edit links, navigation and category boxes, or citation markers. Images are never emitted.
- CSV and text: CSV becomes a table, text stays text.
- The file type is decided from the file's bytes, not from the link or the
Content-Typeheader.
Sample input
{"urls": ["https://www.w3.org/WAI/ER/tests/xhtml/testfiles/resources/pdf/dummy.pdf","https://raw.githubusercontent.com/apache/poi/trunk/test-data/document/SampleDoc.docx","https://raw.githubusercontent.com/apache/poi/trunk/test-data/spreadsheet/SampleSS.xlsx"],"chunking": "by-heading","maxChunkTokens": 800}
This is the prefilled input. In a local run it finished in about two seconds.
Sample output
Real rows from a local run of this Actor (long text shortened with ...).
An XLSX file:
{"schemaVersion": 1,"url": "https://raw.githubusercontent.com/apache/poi/trunk/test-data/spreadsheet/SampleSS.xlsx","finalUrl": "https://raw.githubusercontent.com/apache/poi/trunk/test-data/spreadsheet/SampleSS.xlsx","source": "https://raw.githubusercontent.com/apache/poi/trunk/test-data/spreadsheet/SampleSS.xlsx","format": "xlsx","status": "ok","pages": null,"title": "Sample Spreadsheet","markdown": "## First Sheet\n\n| Test spreadsheet | |\n| --- | --- |\n| 2nd row | 2nd row 2nd column |\n...","chunks": [{"index": 0,"headingPath": ["First Sheet"],"text": "## First Sheet\n\n| Test spreadsheet | |\n| --- | --- |\n| 2nd row | 2nd row 2nd column |\n| This one is red | |","approxTokens": 28,"pageStart": null,"pageEnd": null}],"wordCount": 41,"warnings": ["Sheet \"Sheet3\" is empty."],"error": null,"errorKind": null,"charged": true}
A 15-page research paper (PDF, text layer), shortened:
{"format": "pdf","status": "ok","pages": 15,"title": "Attention Is All You Need","wordCount": 5700,"warnings": ["283 rotated text fragments ignored.","Removed repeated header/footer lines (text that appears at the top or bottom of most pages).","Tables with unreliable columns: table kept as aligned text in a code block on pages 5, 6, 8, 9, 10."],"chunks": [{"index": 5,"headingPath": ["Attention Is All You Need", "3 Model Architecture"],"text": "## 3 Model Architecture\n\nMost competitive neural sequence transduction models have an encoder-decoder structure [5, 2, 35]. Here, the encoder maps an input seq...","approxTokens": 193,"pageStart": 2,"pageEnd": 3}]}
A link that could not be converted keeps its place in the dataset and says why:
{"url": "https://www.w3.org/this-file-does-not-exist.pdf","status": "error","error": "The server answered HTTP 404, so there is no document to convert.","errorKind": "http_error","charged": false}
Input
| Field | Type | Default | What it does |
|---|---|---|---|
urls | list of links | required | Up to 200 public links per run. Duplicates are converted once. |
chunking | by-heading, by-page, none | by-heading | by-heading: one chunk per section with its heading path. by-page: one chunk per PDF page. none: no chunks. |
maxChunkTokens | 100 to 8000 | 800 | No chunk is longer than this. Long sections are split at paragraph boundaries, long tables by rows with the header repeated. Tokens are approximate (characters / 4). |
maxPagesPerDocument | 1 to 2000 | 100 | Only the first N PDF pages are converted. The row says so in warnings. |
includePageMarkers | true / false | true | Adds <!-- page N --> before each PDF page. Chunks keep their page numbers either way. |
Output fields
| Field | Meaning |
|---|---|
schemaVersion | Always 1 for this layout. |
url, finalUrl, source | The link you gave, the address after redirects, and the address the text came from. |
format | pdf, docx, xlsx, html, csv or txt, from the file content. |
status | ok, no_text_layer, empty_after_conversion, error, skipped_charge_limit, or skipped_repeated_failures. |
pages | PDF page count. null for other formats. |
title | Document title if one could be found, else null. |
markdown | The converted document. |
chunks | List of { index, headingPath, text, approxTokens, pageStart, pageEnd }. Page numbers are only set for PDFs. |
wordCount | Words of text, not counting table and heading symbols. |
warnings | Anything the converter skipped or could not do: pages left out, scanned pages, images skipped, thin web pages. |
error | Specific reason when status is error, else null. |
errorKind | Machine-readable reason: http_error, refused_address, download_failed, file_too_large, content_type_mismatch, password_protected, unreadable_document, unsupported_format, empty_file, too_large_uncompressed, html_too_large, html_too_deep, conversion_timeout, conversion_memory, conversion_crashed, charge_limit, circuit_open, document_too_deeply_nested. null when there is no error. |
charged | Whether this document was billed. |
A run summary (SUMMARY in the key-value store) counts converted, no-text-layer, failed and skipped documents.
Pricing
Pay per event: $0.003 for each document converted (event document-converted). The price does not depend on page count or file size.
| Documents | Price |
|---|---|
| 100 | $0.30 |
| 1,000 | $3.00 |
Not charged: results with no text in them (no_text_layer, empty_after_conversion), links that fail (HTTP errors, refused addresses, files over 25 MB, password-protected or damaged files, wrong file type) and PDFs with no text layer. If pay-per-event pricing is not active for a run, converted documents are still delivered, charged stays false, and the run status says so once. Set a maximum cost per run and the Actor stops when it is reached; the remaining links get a row with status: "skipped_charge_limit" and are not converted.
Use with AI agents / MCP
Send this input:
{ "urls": ["https://example.org/report.pdf"], "chunking": "by-heading", "maxChunkTokens": 800 }
Read these fields from each dataset item:
statusmust beok. If it iserror, readerror. If it isno_text_layer, the PDF is a scan and needs OCR elsewhere.chunks[].textwithchunks[].headingPathto index or quote a section.chunks[].pageStartandpageEndgive the PDF pages to cite.warningsfor anything the answer might be missing (skipped pages, skipped images).
Limits
- No OCR. A scanned PDF returns
status: "no_text_layer"and is not charged. If only some pages are scans, the other pages are converted and the empty ones are listed inwarnings. - File size: 25 MB per file; larger files are refused. The XML inside a DOCX or XLSX is checked by its real unzipped size (never the size the file claims): more than 64 MB in one part or 100 MB in total (XLSX: 80 MB) is refused. HTML over 5 MB or nested more than 2,000 levels is refused. A download that stalls or is cut off is an error after 30 seconds, never a partial document.
- Every document is converted in its own process with a 20 second budget and a memory ceiling of 60 percent of the Actor memory shared between the two documents converted at once (307 MB at the default 1024 MB). A document that hangs or needs more is stopped and becomes an error row (
conversion_timeoutorconversion_memory) that is not charged; the other links in the run carry on. The Actor memory is set to 1024 MB. Measured on Linux with Node 20 in a 1 GB container: a 20-page text PDF used 86 MB, a 100-page one 91 MB, real research papers 95 to 113 MB and a 59-page, 48,000-word report 134 MB, each converted in under 5 seconds; a hostile file is stopped within 20 seconds. If 10 documents in a row have to be stopped, the run ends early (circuit breaker) and the rest getstatus: "skipped_repeated_failures", not charged. - Scans: a page counts as having text only above a minimum amount of text (25 characters, 8 for a one-page file). If fewer than 20 percent of the pages have text the result is
no_text_layer; if some pages are image-only the others are converted and the empty pages are named inwarnings. - Text from documents is neutralised: a tag-like
<becomes<, gets a backslash so it cannot run as a formula when you export the dataset to CSV or Excel. The words themselves are never removed. - Password-protected files and legacy Office files (
.doc,.xls,.ppt) are refused with a named error. Save them as DOCX or XLSX without a password first. - Tables in PDFs (best effort): a Markdown table is written only when the columns can be trusted (cells line up under the same columns on every row). Otherwise the rows are kept as a fenced block of aligned text and the run says "table kept as aligned text". A table that continues on the next page, or whose left and right halves were read separately, is joined when the columns clearly match. Merged cells, cells with several wrapped lines and tables drawn as images do not convert well.
- Hyphenated words (heuristic): a hyphen at a line end is dropped only when the joined word looks like one word (it occurs elsewhere in the document, or neither half is a word of its own). It is kept when the next line starts with a capital or digit, when the compound appears hyphenated elsewhere, when the left part is a prefix or one or two letters (pre-, non-, co-), and when both halves are ordinary words (encoder-decoder). It can still be wrong for a rare word.
- Footnotes (best effort): small print at the bottom of a PDF page is moved to a
Footnotes:list after that page's text. Small print that holds table rows, or runs longer than 12 lines, is left in place. Footnotes in two columns can run together. - Figures (best effort): runs of short, scattered labels (axis ticks, diagram boxes) are left out and counted in
warnings. Text inside images is not read. - Page breaks: a paragraph cut by a page break is joined to the first lines of the next page and sits on the first page of the two.
- Headings in PDFs are detected from section numbers, keywords, small capitals, bold text and font size. A heading must start a line with a capital letter, digit or quote and is never a "Figure 1" or "Table 2" caption. A PDF with one font size, no bold and no numbering has no detected headings. Not supported: OCR of scans and images, text inside pictures, and reading forms or annotations.
- DOCX: images are skipped and counted in
warnings. Headers, footers and text boxes are not included. XLSX: charts and images are skipped, hidden sheets are skipped, sheets are cut at 10,000 rows and 100 columns, and formulas show their stored values only. - HTML: only the HTML the server sends is read. A page that builds its content with JavaScript comes back thin and gets a warning.
- Public links only. The Actor does not log in, use cookies or fetch addresses on private networks. Only ports 80 and 443 are used.
- A row can hold at most about 8.5 MB. For a very large document the chunks, then the end of the Markdown, are dropped and a warning says so.
- Each PDF page is converted on its own, so a paragraph that runs over a page break becomes two paragraphs.
FAQ
Which file types do I get? PDF, DOCX, XLSX, HTML, CSV and plain text. The type comes from the file's content. A link that ends in .pdf but serves a login or error page returns an error row instead of a wrong conversion.
Does it read scanned PDFs? No. It reads the text layer only. Scans return no_text_layer and cost nothing.
Can I get one file with all the text? Each row has the whole document in markdown. Use the dataset export (JSON, CSV, Excel) from Apify for a file.
What are the tokens? An estimate of characters divided by 4. It is meant for sizing chunks, not for exact billing by a model provider.
How do I keep page numbers? Leave includePageMarkers on and read pageStart and pageEnd on each chunk.
Why did a PDF come back with a warning about rotated text? Text drawn sideways (watermarks, margin stamps) is skipped. The count is in warnings.
Open-source credits
Licences were read from each package's LICENSE file in the installed version, and the whole production tree was scanned with tools/licence-scan.mjs.
| Package | Version | Licence | Used for |
|---|---|---|---|
| pdfjs-dist | 4.10.38 | Apache-2.0 | Reading text and positions from PDF |
| mammoth | 1.13.0 | BSD-2-Clause | DOCX to HTML |
| jszip | 3.10.1 | MIT (offered as MIT or GPL-3.0; used under MIT) | Opening DOCX and XLSX packages |
| cheerio | 1.0.0 | MIT | Parsing HTML |
| apify | 3.7.2 | Apache-2.0 | Actor runtime |
Counts are for the shipped Linux image (npm install --omit=dev --omit=optional, as in the Dockerfile): 215 production packages in all, 47 of them pulled in by the four conversion libraries. Of the 215: 154 MIT, 19 Apache-2.0, 18 BSD-2-Clause, 9 ISC, 5 BSD-3-Clause, and one or two each of (MIT OR CC0-1.0), 0BSD, BlueOak-1.0.0 (sax), (MIT AND Zlib) (pako), BSD (duck, BSD-2-Clause text), jszip (MIT or GPL-3.0, used under MIT) and CC-BY-4.0 (caniuse-lite browser data from the Apify SDK; attribution given here). Of the 47 under the conversion libraries: 27 MIT, 13 BSD-2-Clause, 2 ISC, 1 BSD-3-Clause, 1 BSD, 1 Apache-2.0, jszip and pako. Nine packages declare an MIT, ISC or Apache-2.0 licence in their metadata but ship no licence file: seven inside the Apify SDK tree, and boolbase (ISC) and isarray (MIT) under the conversion libraries. map-stream (Apify SDK tree) has no licence field but ships an MIT licence text; pause-stream declares MIT and Apache2.