RTF to Markdown API
Send an .rtf file to POST /v2/convert/rtf/to/md and get Markdown back: # and ## headings, - and 1. lists, a pipe table that keeps the RTF's column alignment, inline code for monospaced text, and links with their URLs.
- Endpoint
- POST /v2/
convert/ rtf/ to/ md - Accepts
- rtf
- Sample timing
- 23 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/rtf/to/md \
-H "Authorization: Bearer $PDFCONVERSIONAPI_KEY" \
-F File=@repair-estimate.rtf \
-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/rtf/to/md",
headers={"Authorization": f"Bearer {key}"},
files=[("File", open("repair-estimate.rtf", "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 = "repair-estimate.rtf";
form.append("File", new Blob([await readFile(file)]), file);
const url = "https://api.pdfconversionapi.com/v2/convert/rtf/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.023,
"Files": [
{
"FileName": "repair-estimate.md",
"FileExt": "md",
"FileSize": 1813,
"FileData": "IVtdKGltYWdlLnBuZykKCkFw…"
}
]
}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.
repair-estimate.md, first 30 of 49 lines
April 18, 2027Harbor View Apartments, unit 4B22 Harbor View Road, MillbrookDear resident,# Estimate for refrigerator repairOur technician inspected your refrigerator on April 16. The fresh-food section held 52°F against a set point of 37°F because the evaporator fan motor has failed and the defrost heater is open. Both parts are in stock at our shop, and the repair takes about 1½ hours.| **Item** | **Qty** | **Unit price** | **Amount** || :--- | ---: | ---: | ---: || Evaporator fan motor | 1 | $86.40 | $86.40 || Defrost heater | 1 | $42.15 | $42.15 || Labor (hours) | 1.5 | $95.00 | $142.50 || Service call (paid April 16) | 1 | $65.00 | $65.00 || Subtotal | | | $336.05 || Sales tax on parts (8%) | | | $10.28 || **Total due on approval** | | | **$346.33** |Reference: `EST-2027-0418`. The café cooler in the lobby was checked on the same visit and needs no work.## What the estimate does not cover- Food that spoiled before or during the repair- The ice maker, which was not tested
- Input
- repair-estimate.rtf (14 KB)
- Output
- repair-estimate.md (1.8 KB)
- Source
- A two-page repair estimate letter from a fictional appliance shop, with a logo, a priced table, lists, a link, a footnote and a page-numbered footer. We wrote it for these pages in Rich Text Format. (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 |
|---|---|
FileNamestring | Base name for the Markdown file; the sample is returned as repair-estimate.md. |
StoreFileboolean, default false | Ask for a link to the .md (kept 3 hours) rather than its content in the JSON. |
What carries over, and what doesn't
Kept
- Heading 1 as # and heading 2 as ##
- The priced table as a pipe table with :--- on the item column and ---: on the three number columns, matching the RTF's left and right alignment
- Merged totals rows as a label followed by two empty cells, so every row keeps four columns
- Bold text in the table's header and total row, and bold and italic words in the body
- The Courier New reference EST-2027-0418 as inline code
- The numbered steps, the bullets and the approval link with its full URL
- UTF-8 characters such as °, ½ and é from the RTF's Windows-1252 escapes
Lost or not yet supported
- The logo file: the Markdown starts with , but no image is returned and the alt text is empty.
- Header, footer and footnote text.
- Colors and the highlight: the red deposit warning stays bold but loses its color, and the trip fee is plain.
- Tight paragraph spacing. The two address lines are separate paragraphs in the RTF, so the Markdown puts a blank line between them and they render apart.
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.
Where teams use it
Feeding RTF documents to language models
Letters, notes and estimates saved as RTF become compact text that keeps structure: the two-page sample is 1,813 bytes of Markdown, with the table still in rows and columns and the headings marking each section.
Migrating RTF help files and notes to a docs site
Convert RTF pages to Markdown once and commit them to a repository that a static site generator builds. Lists, links and tables render without hand editing.
Questions
What happens to pictures in the RTF?
Each becomes an image link to image.png, and the image itself is not part of the response. If you need the pictures, take them from the RTF (they are stored as hex after \pngblip or \jpegblip), or use /v2/convert/rtf/to/html, which embeds them.
How are merged table cells written?
Markdown tables can't merge cells, so the merged cell's text goes in the first column and the cells it covered are left empty. The sample's Subtotal, tax and total rows each read as a label, two empty cells and the amount.
Which line endings and encoding does it use?
UTF-8 without a byte order mark and LF line endings, with one blank line between blocks. The TXT route uses CRLF instead.
Convert your first RTF file in a minute.