HTML to RTF API
Send an .html file to /v2/convert/html/to/rtf and get one .rtf file back with the page's headings, lists, table, links and picture. The sample page came back as 8,125 bytes of ANSI-coded RTF, with its PNG written inline as hex.
- Endpoint
- POST /v2/
convert/ html/ to/ rtf - Accepts
- html
- 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/html/to/rtf \
-H "Authorization: Bearer $PDFCONVERSIONAPI_KEY" \
-F File=@seed-swap.html \
-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/html/to/rtf",
headers={"Authorization": f"Bearer {key}"},
files=[("File", open("seed-swap.html", "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 = "seed-swap.html";
form.append("File", new Blob([await readFile(file)]), file);
const url = "https://api.pdfconversionapi.com/v2/convert/html/to/rtf";
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": "seed-swap.rtf",
"FileExt": "rtf",
"FileSize": 8125,
"FileData": "e1xydGYxXGFuc2lcYW5zaWNw…"
}
]
}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.

- Input
- seed-swap.html (3.0 KB)
- Output
- seed-swap.rtf (7.9 KB)
- Source
- A one-page web page for a fictional community seed library, written for this site: headings, a table, two lists, links, a quote, a style sheet, a script and an inlined 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 |
|---|---|
FileNamestring | Sets the returned name; seed-swap.rtf is what the sample gets without it. |
StoreFileboolean, default false | Returns Files[].Url, a download link kept for 3 hours, rather than the RTF inline. |
What carries over, and what doesn't
Kept
- Headings as the
heading 1andheading 2styles: bold, 20 pt and 16 pt, with outline levels - Numbered and bulleted lists through a list table; the nested item sits at level 2 with a ◦ bullet
- The table as 5 cells of 1872 twips (1.3 in) per row, the first row flagged
\trhdras a header, header text bold - The PNG, as
\pict\pngbliphex for 160 × 100 px, sized 2400 × 1500 twips (1.67 × 1.04 in) - Links as HYPERLINK fields, underlined and colored 5, 99, 193,
mailto:included styleattributes: a test paragraph kept Georgia, #cc0000, 18 pt and centering; a cell kept its #eeeeee shading and its two-column span- Accented and non-Latin letters as
\uescapes: Café becameCaf\u233?and 日本 two escapes - The
<title>and meta description in the\infogroup as\titleand\doccomm
Lost or not yet supported
- Style sheet rules. The font table lists only Aptos, so the sample's Georgia and green heading are gone.
- Borders from CSS. The sample's table rows have no
\clbrdrborders; a test table withborder="1"did get them. - Pictures that are links rather than data, which turn into their alt text.
- Script effects and HTML comments.
- Page size and margins from the page. The RTF sets
\paperw12240\paperh15840(Letter) and 1-inch margins.
Limits
- One HTML file per request.
- 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.
- Nothing is downloaded while converting: linked images and CSS files stay out.
Where teams use it
Rich text for programs that read RTF
When the program on the other end imports RTF but not HTML, convert the HTML your system stores and send it a file it can open, with bold, lists and tables intact.
Formatted text in a single plain file
RTF is one text file, with pictures written into it as hex. It fits in a text column or a ticket attachment, where a zipped .docx would need binary handling.
Questions
What does the RTF header look like?
It starts {\rtf1\ansi\ansicpg1252\deff0, followed by a font table, a color table, a style sheet with heading styles, a list table and an \info group. Body text follows as plain paragraphs with \b, \i, \ul and other character controls.
Are pictures kept in the RTF?
Pictures in data: URLs are. The sample's 449-byte PNG is written as hex after \pict\pngblip\picw160\pich100, so the file needs no side files. A picture referenced by URL is replaced by its alt text.
Does it handle non-English text?
Yes. Characters outside the ANSI range are written as Unicode escapes with a ? fallback, for example \u10003? for ✓. UTF-8 pages and pages with a charset meta tag are both read.
Can I choose the page size?
No. There is no page option for this pair, and options such as PageSize=a4 or MarginHorizontal=2 are ignored: each gave a file identical to one made without them. Only the creation time in the \info group changes between runs, so the size can move by a byte or two.
Convert your first HTML file in a minute.