PDF to Markdown API
Send a PDF to POST /v2/convert/pdf/to/md and get a Markdown file back. The converter guesses headings from how the text is set and rebuilds paragraphs and tables. Treat it as a first draft: lists, code, links and images need work, as the sample shows.
- Endpoint
- POST /v2/
convert/ pdf/ to/ md - Accepts
- Sample timing
- 25 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/pdf/to/md \
-H "Authorization: Bearer $PDFCONVERSIONAPI_KEY" \
-F File=@db-rotation-runbook.pdf \
-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/pdf/to/md",
headers={"Authorization": f"Bearer {key}"},
files=[("File", open("db-rotation-runbook.pdf", "rb"))],
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 = "db-rotation-runbook.pdf";
form.append("File", new Blob([await readFile(file)]), file);
const url = "https://api.pdfconversionapi.com/v2/convert/pdf/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.025,
"Files": [
{
"FileName": "db-rotation-runbook.md",
"FileExt": "md",
"FileSize": 1578,
"FileData": "IyBSdW5ib29rOiByb3RhdGlu…"
}
]
}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.
db-rotation-runbook.md, first 30 of 58 lines
# Runbook: rotating the orders database passwordKestrel Logistics platform team. Owner: platform on-call. Last reviewed October 2, 2026.*Kestrel Logistics and its systems are fictional. This sample document was written for pdfconversionapi.com and is released under CC0 1.0.*## When to use this runbookThe orders-app database role gets a new password **every 90 days**. Rotate it early in any of thesecases:• Someone with production database access leaves the company.• The password shows up in a log, a ticket or a chat message.• Security asks for it during an incident.## Before you start1\. Announce the rotation in the *\#platform-changes* channel.2\. Check that replication lag on the read replicas is under 5 seconds.3\. Confirm you hold the db-admin role in Vault.## Steps## 1. Issue the new password
- Input
- db-rotation-runbook.pdf (71 KB)
- Output
- db-rotation-runbook.md (1.5 KB)
- Source
- Written for this site (fictional company); PDF saved by a word processor (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 |
|---|---|
PageRangestring | Convert only these pages. They are cut out of the PDF before the Markdown is built. last stands for the final page. A page past the end returns 400 with the page count; page 0 and a reversed range return 400 too.Values: Pages and ranges such as 1-2,6, 3- or 3-last (to the end) |
Passwordstring | The password of an encrypted PDF. A PDF that forbids copying text, even one that opens without a password, needs its owner password. With PageRange, the open password works only on a file that restricts nothing; on a file that keeps restrictions it returns 400 for now, so send the owner password or remove the protection with /v2/convert/pdf/to/unprotect first. |
FileNamestring | Name for the .md file without its extension; otherwise the PDF's name is reused. |
StoreFileboolean, default false | Get a download link for the Markdown instead of the file itself. Links last 3 hours. |
Not supported yet
Setting one of these to anything but its default returns 400.
| Option | What to do instead |
|---|---|
Images | Only the default, embed, is accepted, and even then images are left out. Get them as files from /v2/convert/pdf/to/extract-images. |
ListBulletChar | Only the default, -, is accepted. Bullets currently come out as the PDF's own • character. |
DetectHeadings | Headings are always detected; turning detection off is not supported yet. |
What carries over, and what doesn't
Kept
- Headings, as # and ## lines guessed from how the text is set (the sample's title and section headings came out right)
- Bold and italic text, as **bold** and *italic*
- Table rows that sit on one page, as a pipe table
- Paragraphs joined onto one line each, with a blank line between them
- Page breaks, as a --- line, so you can still tell where pages ended
- Characters Markdown would misread are escaped, such as \# in "#platform-changes" and 1\. at the start of a numbered line
Lost or not yet supported
- Images. The sample's diagram (its caption is part of the picture) is missing from the Markdown with no placeholder, even though
Images=embedis the default and is accepted. - Link targets. "orders database wiki page" kept its text and lost its URL.
- Lists. Bullets arrive as • characters in separate paragraphs and numbered items as escaped 1\. paragraphs, so neither renders as a Markdown list.
- Third-level headings. The sample's step headings came out as ##, the same level as their parent section.
- Code formatting. Commands set in a monospace font became plain paragraphs, without backticks or fences.
- Tables that cross a page break. In the sample, the header row on page 1 became three bold paragraphs, and on page 2 the first data row became the table header.
- Some sentences split in two at a line wrap: "Rotate it early in any of these" and "cases:" ended up as separate paragraphs.
- Multi-column text. On a set of meeting minutes we tried, a two-column attendance block was read as a table.
Limits
- Uploads up to 10 MB on the Free plan or 100 MB on paid plans; a conversion that runs past 120 seconds returns 504.
- PDFs that need a password to open return 422 "the input file is password-protected" until you send the right
Password. A PDF that forbids copying text returns 422 "this PDF's permissions don't allow copying text and images; send the owner password in Password, or remove the restrictions with /v2/convert/pdf/to/unprotect and the owner password" until its owner password is sent, also when you ask for aPageRange. - Scanned pages carry no text, so a scan comes back as page separators only.
Where teams use it
Preparing PDFs for retrieval and LLM prompts
Markdown headings give you natural places to split a document into chunks, and bold text survives as **bold**. Check a few files first: a sentence can break into two paragraphs at a line wrap, and images and link targets are dropped.
Moving PDF-only docs into a wiki or docs site
Runbooks, policies and manuals often exist only as PDFs. Convert them, fix the lists and code blocks yourself, and commit the result. You start from the full text instead of a blank page, and table rows that fit on one page are already in pipe format.
Reviewing what changed between two PDF versions
Most paragraphs become a single line of Markdown, so a line diff of two converted versions points at the paragraph that changed instead of every line the PDF rewrapped.
Questions
Is the Markdown good enough to feed straight into an LLM or a vector database?
For prose with clear headings, mostly yes: headings, paragraphs and emphasis come through, and paragraphs are unwrapped. Check lists and tables in your own documents before trusting it. In our sample, bullets lost their list syntax, a table that crossed a page break lost its header, and one sentence was split across two paragraphs.
What happens to images in the PDF?
They are left out of the Markdown, with no image link or placeholder. To keep them, call /v2/convert/pdf/to/extract-images on the same PDF. It returned the sample's diagram as a 900 × 220 PNG, the same pixel size as the picture that went into the PDF.
How are pages separated in the output?
With a line containing only ---, which renders as a horizontal rule. Running headers and footers from the PDF stay in the text around it, so strip them if they would confuse your search or chunking.
Can I convert a scanned PDF to Markdown?
Not usefully today. A scan has no text, so the Markdown holds only page separators. Running the scan through /v2/convert/pdf/to/ocr first does add text, but in our test the Markdown built from that OCR layer turned whole lines into table cells. For scans, /v2/convert/pdf/to/ocr with OutputType=txt gives cleaner plain text.
How do I convert a password-protected PDF?
Send its password in Password. The converter respects a PDF's copy restriction, so if the file forbids copying text you need the owner password: in our test the open password alone got a 422 for that. The same applies to PDFs that open without a password but forbid copying. For some pages of a protected file, send the owner password: PageRange=1 with it gave the same Markdown as page 1 of the unprotected runbook. On a file that keeps restrictions, the open password with PageRange returns 400 "PageRange can't be combined with the open password yet for PDF → MD; send the owner password in Password, or remove the protection with /v2/convert/pdf/to/unprotect first"; on one protected with only a UserPassword, the two work together.
Convert your first PDF file in a minute.