---
seo_title: Anything to Markdown API — Any File for RAG | EnConvert
meta_desc: Convert PDF, DOCX, PPTX, XLSX, CSV, HTML, and EPUB files to clean Markdown with POST /v1/convert/anything-to-markdown — heading-aware for LLM and RAG pipelines.
keywords: anything to markdown api, convert pdf to markdown api, docx to markdown api, file to markdown for rag, convert documents to markdown for llm, xlsx to markdown api, pptx to markdown api, epub to markdown api
---

# 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](/docs/endpoints/web-pages/url-to-markdown), and the same converter powers file ingestion in [the ingest endpoint](/docs/v2-ingest) — 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 | PDF | `.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). |

<div class="alert alert-info">
<strong>Note:</strong> Images (<code>.png</code>, <code>.jpg</code>, and similar) are not yet supported on this endpoint — image OCR requires a separate, cost-gated pipeline. Uploading an image returns a <code>400</code> with a clear message. To convert a <em>web page</em> to Markdown instead of a file, use <a href="/docs/endpoints/web-pages/url-to-markdown">url-to-markdown</a>.
</div>

---

## 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](/docs/authentication). 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:

```json
{
    "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 clear `400` — 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 `.doc` is routed through LibreOffice first.
- **PPTX / PPT** — Each slide becomes a `## Slide N: Title` section, body placeholders become bullets, tables become pipe tables, and speaker notes are appended as a `### Notes` subsection — so each slide is an independent, retrievable unit.
- **XLSX / XLS / CSV** — Each worksheet becomes a `## SheetName` heading 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 `.md` upload 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](/docs/v2-ingest) 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`](/docs/v2-ingest#ingesting-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 LangChain `JSONLoader`, LlamaIndex `SimpleDirectoryReader`, or a vector database.
- **Web pages instead of files** — use [`POST /v2/ingest`](/docs/v2-ingest) (crawl or URL list) or [url-to-markdown](/docs/endpoints/web-pages/url-to-markdown) for a single page.

---

## Code Examples

### curl (Private Key)

```bash
curl -X POST https://api.enconvert.com/v1/convert/anything-to-markdown \
  -H "X-API-Key: sk_live_your_private_key" \
  -F "file=@quarterly-report.pdf"
```

### Python (Private Key)

```python
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)

```javascript
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)

```php
$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](/docs/error-codes).

---

## 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](#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](#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`](/docs/v2-ingest#ingesting-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`](/docs/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.
