LlmFiles
Converter to Markdown
LlmFiles/Guides/Batch converting documents to Markdown

Batch converting documents to Markdown

A RAG pipeline, a wiki import or a model fine-tune all start the same way: a folder of mixed documents that has to become Markdown. LlmFiles does that in three ways, and they cost different amounts of the free daily quota. Every number below was measured on one corpus of nine files, on 1 November 2026, and the routes were measured on the same files.

Updated 5 Nov 2026 · LlmFiles

The corpus these numbers come from

Nine files, 60 344 bytes: a Word handbook with a table, a spreadsheet with two sheets and 120 rows, a six-slide deck, a two-page PDF, an HTML page, a CSV export with 300 rows, a JSON schema, a Markdown runbook and a text note. They were written for this test, so the sizes are honest but the documents are not customer files. Output is measured in characters and words of the Markdown that came back.

Three routes, side by side

RouteHow many filesWhere the output goesFree quota
Queue on the upload page20 per drop, two converted at a timedownload per file, or one combined .mdone conversion per file
POST /api/convert/batch20 per requestJSON, Markdown inside the responseone per converted file
POST /api/convert/zip100 members, 100 MBa ZIP of .md files plus a manifestone for the whole archive
CLI on your own machinea folder, recursivefiles next to the source or -ono quota, nothing uploaded

One request, nine files

The batch endpoint took all nine files in a single multipart request and returned in 0.153 s. The same nine files sent one at a time took 0.261 s in total, so on loopback the saving is real but modest — what a batch actually saves over a tunnel is nine round trips and nine quota checks, not nine conversions' worth of work. The response is a JSON object with count, converted, failed, skipped and a files array that carries the Markdown of each file, so one failing file does not lose the other eight.

curl -s -X POST https://llmfiles.com/api/convert/batch \
  -F [email protected] -F [email protected] \
  -D - -o batch.json | grep -i x-batch
# x-batch-total: 2
# x-batch-converted: 2
# x-quota-remaining: 3

The X-Batch-* headers repeat the four counts without parsing the body, which is the cheap way to watch a job. Per-file cost of the endpoint itself is small: twenty tiny files came back in 0.024 s. Twenty-one files are refused outright with HTTP 413 and the sentence "Too many files in one batch: 21, the limit is 20. Split the batch or send the files in a .zip to /api/convert/zip."

What came out, file by file

InputSourceMarkdownWords
airports.csv13 921 B17 871 chars4 526
market-data.xlsx8 702 B5 292 chars1 374
support-handbook.docx1 504 B804 chars157
sales-deck.pptx32 629 B464 chars89
archive-paper.pdf1 845 B655 chars116
api-docs.html748 B404 chars72
openapi.json529 B359 chars52
runbook.md273 B273 chars49
meeting-notes.txt193 B223 chars33

The CSV is the loudest line: 13.9 KB of rows became a 17.9 KB pipe table with 4 526 words, because Markdown pads every cell and repeats the separator row — faithful, and the most character-expensive shape in the corpus. The deck is the opposite: 32.6 KB in, 89 words out, since the rest was XML plumbing around six slides. A PDF returns flat text with no headings at all, which is a property of the format and is covered in the PDF guide.

The quota is per file; the archive is not

This is the part that decides which route you want. A batch charges one conversion per file that converts; an archive charges one for the whole thing. Measured on 01.11, when the daily allowance was five: from an IP with nothing used that day, a batch of seven files returned converted: 5 and skipped: 2, and the two skipped files carried the reason Daily limit reached (5 conversions per day). The request itself was HTTP 200, because five files did convert — the status only becomes 402 when nothing could be converted. Sending the same corpus as a ZIP cost one conversion: the response reported X-Quota-Used: 1 and X-Quota-Remaining: 4 for nine converted files. The allowance has been 25 a day since 01.11, so the same batch of seven now converts in full; the accounting itself did not change.

Practical consequence for the free plan: an archive is the cheap route. A batch of twenty documents spends twenty conversions — four fifths of a day's allowance; the same twenty documents zipped cost a single conversion, as long as the archive stays under 100 members and under 100 MB.

Archives: structure stays, junk is listed

A ZIP is unpacked one member at a time, each member goes through its own converter, and the folder structure is rebuilt in the output. In a test archive of 15 659 bytes with seven members, docs/reports/, docs/ and data/ were preserved, extensions were replaced with .md, and four files converted in 0.083 s. The other three were not dropped silently: the manifest lists them under "Not converted" with a reason each — __MACOSX/._runbook.md and .DS_Store as archive service files, images/logo.png as "binary file, no extractable text".

The manifest — _manifest.md at the top of the output archive — opens with the count ("Converted 4 of 7 files, skipped 3"), then a table of source name, output name and size, then the skipped entries, then a note on how the archive was produced. It is written for a person, not a script, but it is the only record of the members that did not convert. Nested archives are not opened: a ZIP inside a ZIP stays a ZIP at depth 1.

One thing the archive route will not do is appear inside a batch. Send a ZIP as a member of multipart to the batch endpoint and that member comes back failed with "Archives are not unpacked inside a batch: send this .zip on its own to /api/convert/zip." The two endpoints stay separate on purpose, so that one archive cannot quietly turn twenty files into two thousand.

When the folder is bigger than that

The CLI runs the same converter with no upload and no quota. The nine-file corpus took 1.8 s as a folder command, and a hundred copies of it took 6.2 s — about 60 ms per file on this machine, with Python's start-up on top once. Recursion is on by default, --no-clobber protects files that already exist, and --json prints a summary a build script can read.

llmfiles ~/Documents/contracts -o out/ --json
# { "ok": true, "converted": 100, "failed": 0, "skipped": 0, ... }
llmfiles ~/Documents/contracts --no-recursive --no-clobber

Wiring it into a pipeline

For a corpus that has to land in a repository or a vector store, the archive route is the easiest to script: post the ZIP, save the response, unzip it, read the manifest to see which members need attention, then commit. Everything the endpoints accept and return is on the API page, including the error codes: 413 for too many files, 402 when the daily limit leaves nothing to convert, 422 for a file with no extractable text.

Limits, as they actually are

Free: 25 conversions a day per IP, files up to 25 MB, a batch or archive request up to 100 MB (four times the per-file limit), 20 files per batch, 100 members per archive. Files are parsed in memory and no copy is written to disk, and nothing is sent to a third-party cloud. Nothing is sold here: no paid plans, no keys. The page at /pricing explains where the limits come from. The cap exists so one upload cannot take the server down for everybody. The converter itself is on the upload page, and the same code runs in the CLI.

Frequently asked questions

Does a batch count as one conversion?

No. Each file that converts spends one conversion from the daily allowance of 25, so a batch of thirty stops at the twenty-fifth file. The ZIP endpoint is the exception: one archive is one conversion, whatever it contains, up to 100 members.

What happens when the quota runs out halfway through a batch?

The remaining files are returned with skipped and the reason Daily limit reached (25 conversions per day), and the request stays HTTP 200 because earlier files did convert. You get 402 only when the first file is already over the limit and nothing converts at all.

Can I put a ZIP inside a batch request?

No, and the response says so in one sentence. Archives go to /api/convert/zip on their own. The reason is arithmetic: one archive in a batch would make the per-file quota and the 100 MB request cap meaningless.

How do I convert more than 100 files at once?

Zip them in groups of 100, or run the CLI on the folder — it has no file count limit and no quota because nothing is uploaded. A hundred files took 6.2 s as a folder command, about 60 ms per file on one machine.

Is the folder structure kept?

Yes. docs/reports/report.docx becomes docs/reports/report.md inside the output archive, and the CLI does the same into the folder given with -o. Service files such as __MACOSX and .DS_Store are skipped and listed in the manifest rather than copied.

Where do the uploaded files go?

Into memory. The upload is parsed from the request body, the Markdown is written to the response, and no copy of the file is stored on disk or handed to a third-party conversion service. If that is a hard requirement, the CLI does the same work without any upload at all; a packaged container is on the roadmap, not shipped.

Convert a file

Drag a document into the window and get Markdown you can read, diff and feed to a model. Free: 25 files a day, up to 25 MB.