DOCX to MD API
Send a Word file to POST /v2/convert/docx/to/md and get one Markdown file back. Heading styles map to # levels and tables to pipe tables; the sample adds IncludeMarkup=true, which lists the document's comments under a final ## Comments heading.
- Endpoint
- POST /v2/
convert/ docx/ to/ md - Accepts
- docx
- Sample timing
- 26 ms median of 5 runsHow we measure
- Request size
- Up to 10 MB on the Free plan or 100 MB on paid plans, all files together
Request and response
Every sample below ran against the API before this page was published. Set PDFCONVERSIONAPI_KEY to your key and run it as is.
curl -sS -X POST \
https://api.pdfconversionapi.com/v2/convert/docx/to/md \
-H "Authorization: Bearer $PDFCONVERSIONAPI_KEY" \
-F File=@volunteer-handbook.docx \
--form-string IncludeMarkup=true \
-o response.json
# FileData is base64. Save the first file in converted/,
# or print the JSON error.
if jq -e '.Files' response.json > /dev/null; then
mkdir -p converted
name=$(jq -r '.Files[0].FileName' response.json)
jq -r '.Files[0].FileData' response.json \
| base64 --decode > "converted/$name"
else
cat response.json
fiimport base64
import os
import requests
key = os.environ["PDFCONVERSIONAPI_KEY"]
response = requests.post(
"https://api.pdfconversionapi.com/v2/convert/docx/to/md",
headers={"Authorization": f"Bearer {key}"},
files=[("File", open("volunteer-handbook.docx", "rb"))],
data={
"IncludeMarkup": "true",
},
timeout=180,
)
result = response.json()
if not response.ok:
raise SystemExit(f"{result['Code']}: {result['Message']}")
os.makedirs("converted", exist_ok=True)
for file in result["Files"]:
path = os.path.join("converted", file["FileName"])
with open(path, "wb") as out:
out.write(base64.b64decode(file["FileData"]))
print(path, file["FileSize"], "bytes")
import { mkdir, readFile, writeFile } from "node:fs/promises";
import { join } from "node:path";
const form = new FormData();
const file = "volunteer-handbook.docx";
form.append("File", new Blob([await readFile(file)]), file);
form.append("IncludeMarkup", "true");
const url = "https://api.pdfconversionapi.com/v2/convert/docx/to/md";
const key = process.env.PDFCONVERSIONAPI_KEY;
const response = await fetch(url, {
method: "POST",
headers: { Authorization: `Bearer ${key}` },
body: form,
});
const result = await response.json();
if (!response.ok) {
throw new Error(`${result.Code}: ${result.Message}`);
}
await mkdir("converted", { recursive: true });
for (const file of result.Files) {
const path = join("converted", file.FileName);
await writeFile(path, Buffer.from(file.FileData, "base64"));
console.log(path, file.FileSize, "bytes");
}
{
"ConversionCost": 1,
"ConversionTime": 0.026,
"Files": [
{
"FileName": "volunteer-handbook.md",
"FileExt": "md",
"FileSize": 1487,
"FileData": "IyBWb2x1bnRlZXIgaGFuZGJv…"
}
]
}Response from our test run on 2026-11-03. ConversionTime shows the median of 5 runs. FileData is shortened here; the real response carries the whole file as base64.
The sample, converted
The same file the code sends. Download the input and the output and compare them yourself.
volunteer-handbook.md, first 30 of 51 lines
# Volunteer handbookRiverside Tool Library, edition 3, spring 2027**Contents**Opening the library 1Winter hours (removed) 3# Opening the libraryVolunteers open the doors at **9:00** on Saturdays and *10:30* on weekdays. Read the [opening checklist](https://example.org/checklist) before your first shift.- Unlock the front door and switch off the alarm.- Turn on the dust collector.- Check that the filter bag is less than half full.- Count the cash box and write the total in the log.## Shift schedule| **Day** | **Hours** | **Lead volunteer** | **Tools out** || --- | --- | --- | --- || Monday | 16:00 to 19:00 | Amara Diallo | 14 || Wednesday | 16:00 to 19:00 | Jonas Berg | 22 || Saturday | 9:00 to 14:00 | Priya Shah | 41 |# Lending rules
- Input
- volunteer-handbook.docx (43 KB)
- Output
- volunteer-handbook.md (1.5 KB)
- Source
- A two-page volunteer handbook for a fictional tool library, written for this site, with a stale table of contents, a footnote, a comment, a tracked change, a table and a picture. (CC0-1.0). How it was made
- Made by
- 2026-11-04, API build
9eeeb0640aba
Parameters
Send these as form fields next to the file. Names are case-insensitive. If you set one of the documented options below that we don't support yet, the request fails with a 400 that names it instead of returning a file made without it.
| Parameter | What it does |
|---|---|
IncludeMarkupboolean, default false | Add a ## Comments section at the end, one numbered line per comment with the author in bold, the commented text in quotes and the comment itself. Tracked changes are not shown either way: the Markdown holds the text as if every change were accepted. |
UpdateFieldsboolean, default false | Rebuild the table of contents before converting. The entries then become links, such as [Shift schedule 1](#_Toc000000002), and each heading gets a matching <a id> anchor. Without it the TOC is copied as plain lines, stale entries included. |
UpdateTableOfContentsboolean, default false | Same as UpdateFields; so is UpdateReferences. |
Passwordstring | Opens an encrypted .docx. Without it a protected file returns 422. |
FileNamestring | Base name of the Markdown file: FileName=handbook returned handbook.md. |
StoreFileboolean, default false | Answer with a link to the .md that expires after 3 hours. |
Not supported yet
Setting one of these to anything but its default returns 400.
| Option | What to do instead |
|---|---|
Images | embed, the documented default, is accepted, but no picture is embedded yet; none and url return 400. |
DetectHeadings | Headings are taken from Word heading styles; false returns 400. |
ListBulletChar | Bullets are -. * or another character returns 400. |
What carries over, and what doesn't
Kept
- Title and Heading 1 as
#, Heading 2 as##("## Shift schedule", "## Reporting damage") - The shift table as a pipe table with bold header cells and a
| --- |separator - Bulleted items as
-and the lending rules as a1.2.3.list - The link as
[opening checklist](https://example.org/checklist), and**bold**and*italic*runs - The final text of tracked changes: "Power tools go out for 5 days"
- Comments, with
IncludeMarkup=true: "1. **Priya Shah** on “Late returns pay $1 per tool per day.”: Board votes on the late fee in May."
Lost or not yet supported
- Pictures. The banner becomes
, and no image file comes back with the Markdown. - The footnote about closed-toe shoes, and the header and footer.
- List nesting: the second-level bullet is written at the same level as the others.
- Cell shading and the dot leaders of the TOC, which turn into four spaces before the page number.
Limits
- Requests up to 10 MB on the Free plan or 100 MB on paid plans, all files and fields together.
- 120 seconds per conversion, then 504.
- One Markdown file per request; images are not returned as separate files.
Where teams use it
Publishing Word drafts on a docs site
Writers keep drafting in Word, and the site build converts each .docx to Markdown. Headings, links and tables arrive as Markdown syntax that static site generators render.
Review comments as a checklist
With IncludeMarkup=true, every open comment ends up in a numbered list at the bottom of the file, with who wrote it and on which sentence. That list can go straight into an issue or a pull request description.
Chunking documents for retrieval
Markdown headings give a text splitter natural section boundaries. The sample's 44,003-byte DOCX becomes 1,487 bytes of Markdown, with a heading line for each of its six sections.
Questions
How do I keep the images?
The Markdown links to image.png, but the response has only the .md file, even with Images=embed. Until images are returned, take them from the DOCX itself: it is a ZIP, and the pictures are in word/media/ (the sample's banner is word/media/image1.png).
Does it show tracked changes?
No. Deleted text is dropped and inserted text is kept, so the sample reads "5 days" where the tracked change replaced 7 with 5. IncludeMarkup=true adds comments, not revisions.
What happens to the table of contents?
By default the cached entries are copied as plain text, so a stale TOC stays stale: the sample keeps "Winter hours (removed) 3". With UpdateFields=true the TOC is rebuilt from the headings and each entry links to an anchor on its heading.
Are footnotes written as Markdown footnotes?
Not yet. The footnote text and its reference mark are both left out, so check documents that rely on footnotes before you publish the Markdown.
Convert your first DOCX file in a minute.