Anything to Markdown API#
The POST /v1/convert/anything-to-markdown endpoint converts almost any uploaded document — PDF, Word, PowerPoint, Excel, CSV, HTML, EPUB, plain text, and OpenDocument files — into clean, heading-aware Markdown. It auto-detects the format from the file you send, dispatches to the right extractor, and returns one .md file with the structure LLM and RAG pipelines actually need: real #/##/### headings, GitHub-Flavored pipe tables, fenced code blocks, and resolved links. It is the file counterpart of url-to-markdown, and the same converter powers file ingestion in the ingest endpoint — so the Markdown you get here is exactly what the semantic chunker consumes.
One endpoint replaces a stack of per-format libraries (pdfplumber, mammoth, python-pptx, openpyxl): upload a file, get Markdown, feed it to your embeddings.
Endpoint#
POST /v1/convert/anything-to-markdown
Content-Type: multipart/form-data
Output format: Markdown (.md, UTF-8). The format is auto-detected from the uploaded filename's extension and its magic bytes — you do not specify an input type.
Supported input formats#
The input type is detected from the file extension (validated against the file's magic bytes). These are every format the endpoint accepts:
| Category | Formats | Extensions | Notes |
|---|---|---|---|
.pdf |
Headings inferred from font size; tables extracted as pipe tables; running headers/footers de-duplicated. Scanned/image-only PDFs have no text layer and are rejected — OCR is not performed. | ||
| Word | DOCX, DOC | .docx, .doc |
Word heading styles become #/##/###; lists and tables preserved. .doc (legacy) is handled through LibreOffice. |
| Presentations | PPTX, PPT | .pptx, .ppt |
One ## Slide N section per slide, body bullets, tables, and speaker notes. .ppt (legacy) via LibreOffice. |
| Spreadsheets | XLSX, XLS, CSV | .xlsx, .xls, .csv |
One ## SheetName heading plus a GFM table per sheet. Cell values are kept verbatim (leading zeros and codes are not reformatted). |
| E-books | EPUB | .epub |
Chapters concatenated in reading (spine) order; the table-of-contents/nav document is skipped. |
| Web / markup | HTML, XHTML | .html, .htm, .xhtml |
Faithful full-document conversion (not article-only extraction). |
| OpenDocument | ODT, ODS, ODP | .odt, .ods, .odp |
Text, spreadsheet, and presentation documents, via LibreOffice. |
| Rich text | RTF | .rtf |
Converted through LibreOffice. |
| Plain text | Text, Markdown | .txt, .text, .md, .markdown, .mdown, .mkd |
Encoding-normalised passthrough (Markdown is already valid output). |
.png, .jpg, and similar) are not yet supported on this endpoint — image OCR requires a separate, cost-gated pipeline. Uploading an image returns a 400 with a clear message. To convert a web page to Markdown instead of a file, use url-to-markdown.
Authentication#
This endpoint supports both private and public key authentication.
Private Key#
Include your secret key in the X-API-Key header. Use this for server-to-server calls where the key is never exposed to the client.
X-API-Key: sk_live_your_private_key
Public Key with JWT#
For client-side usage, first generate a JWT token with your public key, then pass it as a Bearer token.
Step 1 -- Get a token:
POST /v1/auth/token
X-API-Key: pk_live_your_public_key
Step 2 -- Use the token:
Authorization: Bearer eyJhbGciOiJIUzI1NiIs...
The full flow, including domain locking and token refresh, is in the authentication guide. Each API key carries an allowed-endpoints allowlist — if /v1/convert/anything-to-markdown is not on the key's list, the request is rejected with 403.
Request Parameters#
Send the file as multipart/form-data. The document goes in the file field; the rest are optional form fields.
| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
file |
file | Yes | -- | The document to convert. Its extension must be one of the supported input formats above. |
output_filename |
string |
No | Input name | Custom name for the output file. The .md extension is added automatically. |
direct_download |
boolean |
No | true |
Accepted for request-shape parity with the other conversion endpoints. |
job_id |
string |
No | -- | Client-provided job ID for timeout recovery. When a sync conversion exceeds reverse-proxy timeout limits, poll GET /v1/convert/status/{job_id} to retrieve the result. |
Response#
The conversion runs in-process and the resulting .md file is stored, then the endpoint returns a JSON body with a pre-signed download URL:
{
"presigned_url": "https://spaces.example.com/...signed...",
"object_key": "env/files/{project_id}/anything-to-markdown/report_20260714_101530123.md",
"filename": "report_20260714_101530123.md",
"file_size": 5124,
"conversion_time_seconds": 1.8,
"job_id": null
}
The same values are echoed in response headers (X-Object-Key, X-File-Size, X-Conversion-Time, X-Filename) so you can read them without parsing the body. Download the Markdown from presigned_url — the link is short-lived.
How each format is converted#
Every format is normalised into the same clean Markdown shape, so downstream chunking and embedding see one consistent structure regardless of the source.
- PDF — Text is extracted with word-level positioning. The most common font size is treated as body text; larger sizes become
#/##/###headings (largest first). Tables are detected geometrically and emitted as GFM pipe tables, and their text is removed from the prose stream so nothing is duplicated. Lines that repeat on most pages (running headers/footers) are dropped. A PDF with no text layer (a scan or image-only export) is rejected with a clear400— this endpoint does not run OCR. - DOCX / DOC — Word's "Heading 1/2/3" paragraph styles map cleanly to
#/##/###; lists, tables, bold, and italic are preserved. Legacy.docis routed through LibreOffice first. - PPTX / PPT — Each slide becomes a
## Slide N: Titlesection, body placeholders become bullets, tables become pipe tables, and speaker notes are appended as a### Notessubsection — so each slide is an independent, retrievable unit. - XLSX / XLS / CSV — Each worksheet becomes a
## SheetNameheading plus a GFM table; a CSV is a single table. Cells are read as strings, so IDs, codes, and leading zeros are preserved exactly. - EPUB — Content documents are read in spine (reading) order and converted chapter by chapter; the navigation/table-of-contents document is skipped so it does not pollute your chunks.
- HTML / XHTML — The whole document is converted faithfully (not reduced to a single "article"), with script/style/noscript stripped and headings, lists, tables, links, and code preserved.
- ODT / ODS / ODP / RTF — Converted through the same LibreOffice engine that powers the document-to-PDF endpoints.
- Text / Markdown — Line endings and encoding are normalised; a
.mdupload passes through essentially unchanged.
Output format#
The result is GitHub-Flavored Markdown built for machine consumption:
- Headings — ATX (
#,##,###), used as semantic section boundaries. - Tables — GFM pipe tables (kept atomic, never split), with
|in cells escaped. - Code — Fenced blocks with language hints where detectable.
- Structure — Paragraphs, lists, and blockquotes preserved; per-slide and per-sheet sections give even flat formats real boundaries.
Because the headings are real ATX headings, this output feeds straight into a heading-aware chunker. If you want the chunks — not just the Markdown — send the file to the ingest endpoint instead, which runs this exact conversion and then splits the result into RAG-ready JSONL.
Building a RAG pipeline#
This endpoint converts one file at a time to Markdown. For a full retrieval pipeline you usually want chunks, not raw Markdown, and often many files at once:
- One file, just the Markdown — use this endpoint, then chunk it yourself.
- Many files, chunked into JSONL — use
POST /v2/ingest/files. It runs the same conversion on every uploaded file, splits each into heading-aware chunks, and assembles one JSONL file that loads directly into LangChainJSONLoader, LlamaIndexSimpleDirectoryReader, or a vector database. - Web pages instead of files — use
POST /v2/ingest(crawl or URL list) or url-to-markdown for a single page.
Code Examples#
curl (Private Key)#
curl -X POST https://api.enconvert.com/v1/convert/anything-to-markdown \
-H "X-API-Key: sk_live_your_private_key" \
-F "[email protected]"
Python (Private Key)#
import requests
with open("quarterly-report.pdf", "rb") as f:
response = requests.post(
"https://api.enconvert.com/v1/convert/anything-to-markdown",
headers={"X-API-Key": "sk_live_your_private_key"},
files={"file": ("quarterly-report.pdf", f)},
)
response.raise_for_status()
result = response.json()
# Download the Markdown from the pre-signed URL.
markdown = requests.get(result["presigned_url"]).text
print(markdown)
Node.js (Private Key)#
import { readFile } from "node:fs/promises";
const form = new FormData();
form.append(
"file",
new Blob([await readFile("quarterly-report.pdf")]),
"quarterly-report.pdf"
);
const response = await fetch(
"https://api.enconvert.com/v1/convert/anything-to-markdown",
{ method: "POST", headers: { "X-API-Key": "sk_live_your_private_key" }, body: form }
);
const result = await response.json();
const markdown = await fetch(result.presigned_url).then((r) => r.text());
console.log(markdown);
PHP (Private Key)#
$ch = curl_init("https://api.enconvert.com/v1/convert/anything-to-markdown");
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => ["X-API-Key: sk_live_your_private_key"],
CURLOPT_POSTFIELDS => [
"file" => new CURLFile("quarterly-report.pdf", "application/pdf", "quarterly-report.pdf"),
],
]);
$data = json_decode(curl_exec($ch), true);
curl_close($ch);
echo $data["presigned_url"];
Error Responses#
| Status | Condition |
|---|---|
400 Bad Request |
Unsupported file type, an empty file, an image upload (OCR not supported), or a document with no extractable text (e.g. a scanned PDF). |
400 Bad Request |
The file's bytes do not match its declared extension (magic-byte mismatch). |
401 Unauthorized |
Missing or invalid API key / JWT token. |
402 Payment Required |
Monthly conversion limit or storage limit reached. |
403 Forbidden |
/v1/convert/anything-to-markdown is not in the API key's allowed endpoints. |
413 Payload Too Large |
The upload exceeds your plan's per-file size limit. |
500 Internal Server Error |
Conversion failed unexpectedly. |
The full status-code reference is in the error-codes guide.
Limits#
| Limit | Value |
|---|---|
| Maximum upload size | Plan-dependent (Free: 5 MB) |
| Monthly conversions | Plan-dependent (Free: 100) |
| File retention | Plan-dependent (Free: 1 hour) |
| PDF page cap | 2,000 pages |
| Supported input formats | See the supported input formats table |
Frequently asked questions#
How do I convert a PDF to Markdown with an API?#
Send a POST request to /v1/convert/anything-to-markdown as multipart/form-data with the PDF in the file field and your key in the X-API-Key header. You get back JSON with a presigned_url to a .md file whose headings come from the PDF's font sizes and whose tables are GitHub-Flavored pipe tables. Scanned/image-only PDFs have no text layer and are rejected — this endpoint does not run OCR.
Which file formats can I convert to Markdown?#
PDF, Word (.docx, .doc), PowerPoint (.pptx, .ppt), Excel (.xlsx, .xls), CSV, EPUB, HTML/XHTML, OpenDocument (.odt, .ods, .odp), RTF, and plain text/Markdown. The full list with per-format notes is in the supported input formats table. Images are not yet supported.
Can I convert documents to Markdown for a RAG or LLM pipeline?#
Yes — that is what the output is built for: real #/##/### headings, atomic pipe tables, and fenced code. For a single file you get the Markdown here and chunk it yourself; for many files chunked into ready-to-embed JSONL, send them to POST /v2/ingest/files, which runs this same conversion and splits each file into RAG-ready chunks.
How is this different from Microsoft MarkItDown or docling?#
It solves the same "any file to Markdown" problem as a hosted API — no local install, no heavy ML dependencies, and heading-aware output tuned for chunking. It pairs directly with /v2/ingest so the same conversion feeds an end-to-end RAG pipeline (convert → chunk → JSONL) without wiring libraries together yourself.
Do you convert scanned PDFs or images?#
Not on this endpoint. A PDF must have a real text layer; a scanned/image-only PDF, or an image file (.png, .jpg), is rejected with a clear 400 because OCR is a separate, cost-gated pipeline. Born-digital PDFs and the other listed document formats convert cleanly.