LlmFiles
Converter to Markdown
LlmFiles/API

LlmFiles API

One POST request, one Markdown document back. No SDK, no sign-up, no key: the free quota covers your first files. Everything below is what you need to put conversion into a pipeline: examples, limits, headers and error codes.

Updated Oct 23, 2026 · LlmFiles

At a glance

A machine-readable schema is live at /openapi.json too: code generators build clients for any language from it. This page covers the same ground in prose.

First request in 10 seconds

curl -F "[email protected]" \
  https://llmfiles.com/api/convert

The response is a single JSON object. The Markdown sits in the markdown field as one string with real newlines, so jq -r .markdown gives you a finished file:

curl -s -F "[email protected]" \
  https://llmfiles.com/api/convert \
  | jq -r .markdown > report.md

The file extension is not decoration: it selects the converter. report.pdf is parsed as PDF, report.docx as Word, and a file with no extension goes to plain-text extraction or to the fallback path.

What the response contains

markdownstringThe result: # headings, lists, tables in pipe format, fenced code blocks.filenamestringThe name you sent. Handy when you convert a batch and collect the results into a dict keyed by filename.titlestringDocument title from metadata (PDF, DOCX, EPUB). Falls back to the file name when there is no metadata. Good value for a title field in a knowledge base.sizenumberSource size in bytes: for a log line, or to measure what you saved.fallbackbooleantrue means the format was not recognized and the payload is raw text or a stub carrying the file size. Check the flag before the output goes into a production corpus.archiveobjectZIP only: found is how many files were inside, converted how many became Markdown, skipped the list with reasons, truncated whether limits were hit.filesarrayZIP only: one element per file — name (path inside the archive), out (name of the resulting .md), size, fallback. Useful for mapping results into your own knowledge base.
Check fallback: true means the converter could not handle the input and returned the contents as-is. This does not happen for PDF, DOCX, PPTX, XLSX, EPUB, HTML, CSV, JSON or source code; for exotic formats it does.

Response headers let the client track what is left without a second call:

X-Planfree, proPlan the request was processed under.X-Quota-Used3Conversions made today from your IP.X-Quota-Remaining2 or unlimitedRemaining until the end of the day. Pro and internal requests report unlimited.X-Archive-Files3Archive only: how many files became Markdown.X-Archive-Skipped1Archive only: how many files were skipped (reasons in archive.skipped or _manifest.md).X-Archive-Truncated0 or 11 means the archive was not processed in full: a limit on file count, total size or time was hit.

Limits

Free5 per IP25 MBNothing to do.Prounlimited300 MBX-License-Key header or the cookie set when you activate a key.Team / On-premunlimited300 MBSame, plus a local install following the instructions.

The day is the server calendar date (UTC+4) and the counter is keyed to the IP: cf-connecting-ip behind Cloudflare, otherwise the first address in X-Forwarded-For. Cookies play no part in quota counting: otherwise clearing cookies would buy a fresh limit on every request. You can read your own IP from the used field of the /api/license response.

Hit the limit and the server returns 402 with a message about 5 files per day. That is not a client error: retrying is pointless until the next day, so the plan is what has to change.

ZIP archives: a batch of files in one request

Available since version 1.4.0. Inside the archive every file is parsed by its own converter: DOCX stays a document with headings, CSV becomes a table, HTML becomes text, and the output holds a separate .md per file. The folder structure is preserved and extensions are replaced with .md.

curl -F "[email protected]" \
  https://llmfiles.com/api/convert/zip \
  -o docs-markdown.zip

unzip -o docs-markdown.zip -d out
Extraction depthone level: a nested archive is left alone and lands in the skipped listFiles per requestup to 100File sizeas on your plan: 25 MB on Free, 300 MB on ProArchive sizeup to 4x the file limit: 100 MB on Free, 300 MB on ProTime90 seconds per archive; whatever did not finish is listed in the manifest instead of being dropped silentlyBinaries with no textPNG, fonts and the like are skippedOS junk__MACOSX, .DS_Store, Thumbs.db are skippedUnsafe paths.. and absolute paths are rejected, so an archive cannot write files anywhere elsePassword-protected filesnot opened; the reason is recordedQuotaone archive counts as one conversion, however many files are inside

Alongside the .md files the output archive contains _manifest.md: a table of archive file → .md file → size, plus the list of skipped entries with reasons. That way you can process a batch without checking anything by hand.

Need one continuous document rather than an archive? Send the same .zip to /api/convert: the response holds stitched Markdown (up to 4 MB, beyond that a note that not every file is shown) plus the archive and files fields for programmatic use.

Batch: many files in one request

Available since version 1.7.0. One request carries up to 20 files under the same field name files. Each file gets its own converter and its own result, so one bad file does not sink the whole batch, and the caller can retry exactly the entries that failed.

curl -F "[email protected]" -F "[email protected]" -F "[email protected]" \
  https://llmfiles.com/api/convert/batch \
  | jq '{converted, failed, skipped, files: [.files[] | {filename, ok}]}'

The response holds counters, a per-file array in the same order as the request, and the quota state after the batch:

{
  "count": 3,
  "converted": 2,
  "failed": 1,
  "skipped": 0,
  "files": [
    {"filename": "report.pdf", "ok": true, "title": "report", "markdown": "# …", "fallback": false, "size": 812345},
    {"filename": "notes.docx", "ok": true, "title": "notes", "markdown": "## …", "fallback": false, "size": 12034},
    {"filename": "data.csv", "ok": false, "error": "Empty file", "size": 0}
  ],
  "quota": {"plan": "free", "limit": 5, "used": 2, "remaining": 3}
}
The web interface does the same thing in the browser: drop a folder of files and they are queued two at a time, with a progress bar, a status per file and one combined .md for everything that converted.

Python

import requests

API = "https://llmfiles.com/api/convert"

def to_markdown(path: str, license_key: str | None = None) -> str:
    headers = {"X-License-Key": license_key} if license_key else {}
    with open(path, "rb") as fh:
        r = requests.post(API, files={"file": (path, fh)}, headers=headers, timeout=300)
    if r.status_code == 402:
        raise RuntimeError("daily limit reached: " + r.json()["detail"])
    r.raise_for_status()
    data = r.json()
    if data["fallback"]:
        print("unrecognized format:", data["filename"])
    return data["markdown"]

print(to_markdown("report.pdf")[:400])

When converting a batch, read X-Quota-Remaining after each file so the loop stops on its own instead of on a 402.

JavaScript

async function toMarkdown(file, licenseKey) {
  const body = new FormData();
  body.append('file', file);
  const res = await fetch('https://llmfiles.com/api/convert', {
    method: 'POST',
    body,
    headers: licenseKey ? { 'X-License-Key': licenseKey } : {},
  });
  if (!res.ok) throw new Error(`HTTP ${res.status}: ${(await res.json()).detail}`);
  return (await res.json()).markdown;
}

document.querySelector('input[type=file]').onchange = async (e) => {
  const md = await toMarkdown(e.target.files[0]);
  console.log(md.slice(0, 500));
};

CORS is open to all origins (Access-Control-Allow-Origin: *), so you can call the API straight from a browser on your own domain, without a proxy. Do not hardcode your license key in client-side code: it is visible in the page source.

License key

Two equivalent ways to pass a Pro key. The header is the better fit for scripts and bots:

curl -F "[email protected]" \
  -H "X-License-Key: MIW-PRO-XXXXXXXX-XXXXXXXX" \
  https://llmfiles.com/api/convert

The cookie is for the browser: POST /api/license/activate with a body of {"key": "MIW-PRO-…"} sets it for a year. A revoked key fails activation (404), and from then on requests fall back to the free quota.

One request confirms that the key took effect: the response reports plan: pro and remaining: null:

curl -s -H "X-License-Key: MIW-PRO-XXXXXXXX-XXXXXXXX" \
  https://llmfiles.com/api/license | jq '{plan, remaining}'

Other endpoints

POST /api/convert/zipUnpack an archive and return a .zip with a .md per fileapplication/zip: files + _manifest.mdPOST /api/convert/batchConvert up to 20 files in one request, with a result per fileconverted, failed, skipped, files[], quotaGET /api/formatsSupported extensions with descriptions{"formats": [...], "count": 24}GET /api/licensePlan, remaining quota, limits, pricesplan, used, remaining, plansPOST /api/license/activateSet the cookie from a key{"ok": true, "plan": "pro"}POST /api/license/deactivateRemove the cookie{"ok": true, "plan": "free"}POST /api/orderJoin the Pro waitlist (checkout is not connected yet)order_id, amount, payments_enabledGET /api/order/{id}Order status, and the key if the order is paidstatus, keyPOST /api/downloadReturn the Markdown as a file (filename + markdown in the form)text/markdown with Content-DispositionGET /healthzService liveness for monitoring{"ok": true, "version": "…"}

There is no conversion-ready webhook: requests are synchronous and the response arrives on the same connection. For batches, send up to 20 files to POST /api/convert/batch in one request and read the per-file results; for anything larger, loop over your files and pause when X-Quota-Remaining reaches zero.

Error codes

400The file field is missing or the file is emptyCheck that the form sends a field named file and that the file is not 0 bytes.402The daily free quota is used upWait for the next day or get a Pro key.413The file exceeds the limit of your plan25 MB on Free, 300 MB on Pro. Split the PDF into parts or compress it.404/api/order/{id} not found, or the key will not activateCheck the order id, and that the key has not been revoked.422FastAPI saw no multipart field, or there is nothing convertible in the archive (binaries only)Send multipart/form-data, not JSON with a file path. For an archive, check that it holds documents and not just images.500Internal converter errorRetry once; if it keeps happening, send us the file name and format and we will look into it.
The message is in detail: {"detail": "File exceeds the 25 MB limit for the free plan."}. Show it to the user as-is: plain prose, no error codes.

Privacy and data

More on why this matters and how to verify it yourself is in the guide PDF to Markdown without the cloud.

Frequently asked questions

Do I need an API key to start?

No. The free tier allows 5 files per day per IP, up to 25 MB each. A key is only needed to lift the limit and raise the file size to 300 MB.

Is there an SDK or a library?

There is no separate SDK, and none is needed: this is a single multipart request, three lines in any language. The machine-readable schema is at /openapi.json, and curl, Python and JavaScript examples are on this page.

What happens to a ZIP archive?

It gets unpacked. POST /api/convert/zip returns a .zip containing one .md per file with the folder structure preserved, plus _manifest.md listing what was skipped. Limits: up to 100 files, each within the plan's file limit, total size up to 4x the file limit, and nested archives are not entered. One archive deducts a single conversion from the quota, not one per file.

Does a call to /api/license count against the quota?

No. The quota is only spent on successful /api/convert calls. Status checks, the format list and the health check are free and unlimited.

Can I call the API from a browser on my own site?

Yes, CORS is open to all origins. But do not put a license key in client-side code: it will be visible in the page source. For browser use, keep a proxy on your own server.

Try it without a key

5 files per day free, up to 25 MB, no sign-up. Files are not stored on the server.