XLS to MD API
Upload an .xls workbook to POST /v2/convert/xls/to/md and receive a Markdown file with one table per sheet, each under a ## heading. The sample's Fuel and Vans sheets fill 1,376 bytes.
- Endpoint
- POST /v2/
convert/ xls/ to/ md - Accepts
- xls
- Sample timing
- under 1 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/xls/to/md \
-H "Authorization: Bearer $PDFCONVERSIONAPI_KEY" \
-F File=@van-fuel-log.xls \
-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/xls/to/md",
headers={"Authorization": f"Bearer {key}"},
files=[("File", open("van-fuel-log.xls", "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 = "van-fuel-log.xls";
form.append("File", new Blob([await readFile(file)]), file);
const url = "https://api.pdfconversionapi.com/v2/convert/xls/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,
"Files": [
{
"FileName": "van-fuel-log.md",
"FileExt": "md",
"FileSize": 1376,
"FileData": "IyMgRnVlbAoKfCBEYXRlIHwg…"
}
]
}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.
van-fuel-log.md, 26 lines
## Fuel| Date | Van | Odometer | Gallons | Price per gallon | Cost | Miles per gallon | Notes || --- | --- | --- | --- | --- | --- | --- | --- || 2026-06-01 | Van 1 | 48210 | 14.2 | 3.689 | 52.3838 | 16.619718309859156 | || 2026-06-02 | Van 2 | 61377 | 16.8 | 3.689 | 61.9752 | 15.595238095238095 | || 2026-06-04 | Van 3 | 30554 | 12.5 | 3.719 | 46.4875 | 18.24 | || 2026-06-08 | Van 1 | 48463 | 15.1 | 3.749 | 56.6099 | 16.75496688741722 | || 2026-06-09 | Van 2 | 61598 | 15.9 | 3.749 | 59.6091 | 13.89937106918239 | Receipt missing;<br>entered from bank statement || 2026-06-11 | Van 3 | 30791 | 13 | 3.759 | 48.867 | 18.23076923076923 | || 2026-06-15 | Van 1 | 48701 | 14.6 | 3.729 | 54.4434 | 16.3013698630137 | || 2026-06-16 | Van 2 | 61859 | 18.4 | 3.729 | 68.6136 | 14.184782608695654 | Idled during ice delivery || 2026-06-18 | Van 3 | 31043 | 12.9 | 3.699 | 47.7171 | 19.53488372093023 | || 2026-06-22 | Van 1 | 48950 | 15.3 | 3.699 | 56.5947 | 16.274509803921568 | || Total | | | 148.7 | | 553.3013 | | |## Vans| Sunrise Rye Bakery: delivery vans | | | || --- | --- | --- | --- || Fictional business. Sample file for pdfconversionapi.com, CC0 1.0. | | | || | | | || Van | Model year | Tank (gal) | Route || Van 1 | 2019 | 24 | North loop || Van 2 | 2021 | 26 | Downtown cafes || Van 3 | 2017 | 22 | Farmers markets |
- Input
- van-fuel-log.xls (11 KB)
- Output
- van-fuel-log.md (1.3 KB)
- Source
- A fictional bakery's van fuel log in Excel 97-2003 format (two sheets, formulas, a cell note and a merged title) that we wrote for this site (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 |
|---|---|
SheetHeadingsboolean, default true | Put the sheet name as a ## heading above each table; false leaves the tables bare. With WorksheetName=Vans as well, the output is a single 324-byte table. |
FirstRowIsHeaderboolean, default true | Take each sheet's first row as the table header. false heads the table with column letters A, B, C. |
WorksheetNamestring | Convert one sheet by name instead of all of them; case is ignored. |
WorksheetIndexinteger | Convert the sheet at this position, from 1.Values: 1 to the number of sheets |
ActiveWorksheetOnlyboolean, default false | Convert only the sheet selected when the file was saved. For the sample that is Fuel, and the output shrinks to 1,043 bytes. |
Not supported yet
Setting one of these to anything but its default returns 400.
| Option | What to do instead |
|---|---|
CellValues | raw and other values return 400; formatted is accepted, though cells hold stored values, not the displayed text. |
IncludeHiddenRows | true returns 400 "IncludeHiddenRows is not supported yet for XLS → MD". Hidden rows are written in any case. |
IncludeHiddenColumns | true returns 400; hidden columns are written in any case. |
Images | Only embed is accepted; other values return 400 "Images is not supported yet for XLS → MD". |
What carries over, and what doesn't
Kept
- Both sheets in tab order under
## Fueland## Vans - Line breaks inside a cell as
<br>: the note reads "Receipt missing;<br>entered from bank statement" - Dates in ISO form (2026-06-01) and whole numbers without a decimal point (48210)
- Empty cells as empty table cells, so the Total row lines up with the header
Lost or not yet supported
- Number formats: prices and costs lose their dollar signs and are not rounded, so the miles-per-gallon column shows values like 16.619718309859156.
- Merged cells: the Vans title becomes a header row followed by empty cells, and the table's real header ends up as a data row.
- The cell note on A6, the drop-down list and the conditional highlight.
Limits
- Excel's binary .xls format only. HTML saved under a .xls name returns 422 "the input file could not be read: Cannot detect file format".
- A password-protected .xls returns 400 "password-protected XLS files can't be converted to MD yet" even with the right
Password, and 422 without one.Passwordsent with an unprotected file is accepted and changes nothing. Re-save a protected file with/v2/convert/xls/to/xlsxandPasswordfirst, then convert the XLSX. - Requests up to 10 MB on the Free plan or 100 MB on paid plans; a conversion has 120 seconds before a 504.
Where teams use it
Reading old workbooks in a pull request or a wiki
Commit the Markdown next to the .xls so reviewers can see what a legacy file contains without opening Excel.
Feeding legacy spreadsheets to a language model
Every sheet arrives as a labeled table of plain values in a small text file, 1,376 bytes for the sample, instead of a binary workbook.
Questions
Can I get just one sheet?
Yes: WorksheetName, WorksheetIndex or ActiveWorksheetOnly=true each limit the output to a single table. Without them, every sheet is converted, hidden sheets included.
Why are the numbers longer than in Excel?
Markdown cells hold the value stored in the file, and the sheet's number format only shows it rounded. The fuel log's 16.6 is stored as 16.619718309859156, and that full value is what you get.
How are merged title cells handled?
The text stays in the first cell of the merged range and the other cells are empty. A title above a table therefore becomes the table header, as on the sample's Vans sheet.
Convert your first XLS file in a minute.