XLSX to MD API
Send a workbook to POST /v2/convert/xlsx/to/md and get one Markdown file: a ## heading per worksheet followed by a pipe table of its cells. The sample's three sheets, the hidden one included, become a 1,776-byte file.
- Endpoint
- POST /v2/
convert/ xlsx/ to/ md - Accepts
- xlsx
- 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/xlsx/to/md \
-H "Authorization: Bearer $PDFCONVERSIONAPI_KEY" \
-F File=@tool-library-loans.xlsx \
-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/xlsx/to/md",
headers={"Authorization": f"Bearer {key}"},
files=[("File", open("tool-library-loans.xlsx", "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 = "tool-library-loans.xlsx";
form.append("File", new Blob([await readFile(file)]), file);
const url = "https://api.pdfconversionapi.com/v2/convert/xlsx/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": "tool-library-loans.md",
"FileExt": "md",
"FileSize": 1776,
"FileData": "IyMgTG9hbnMKCnwgTG9hbiB8…"
}
]
}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.
tool-library-loans.md, first 30 of 38 lines
## Loans| Loan | Item | Category | Member | Out | Due | Days | Deposit | Returned | Shelf || --- | --- | --- | --- | --- | --- | --- | --- | --- | --- || L-1041 | Cordless drill | Power tools | M-208 | 2026-09-01 | 2026-09-08 | 7 | 25 | true | B2 || L-1042 | Hedge trimmer | Garden | M-114 | 2026-09-02 | 2026-09-09 | 7 | 10 | true | D4 || L-1043 | Extension ladder, 24 ft | Ladders | M-301 | 2026-09-03 | 2026-09-06 | 3 | 15 | true | Yard || L-1044 | Orbital sander | Woodworking | M-208 | 2026-09-05 | 2026-09-12 | 7 | 20 | false | C1 || L-1045 | Leaf blower | Garden | M-077 | 2026-09-08 | 2026-09-15 | 7 | 10 | true | D2 || L-1046 | Tile saw | Power tools | M-152 | 2026-09-10 | 2026-09-17 | 7 | 25 | false | B5 || L-1047 | Pole saw | Garden | M-114 | 2026-09-12 | 2026-09-14 | 2 | 10 | true | D3 || L-1048 | Step ladder, 6 ft | Ladders | M-020 | 2026-09-14 | 2026-09-21 | 7 | 15 | true | Yard || L-1049 | Router, 1/4 in collet | Woodworking | M-266 | 2026-09-18 | 2026-09-25 | 7 | 20 | false | C3 || L-1050 | Pressure washer | Power tools | M-301 | 2026-09-21 | 2026-09-28 | 7 | 25 | true | B1 || L-1051 | Wheelbarrow | Garden | M-077 | 2026-09-24 | 2026-10-01 | 7 | 10 | false | Yard || L-1052 | Biscuit joiner | Woodworking | M-152 | 2026-09-28 | 2026-10-05 | 7 | 20 | false | C2 |## Summary| Loans by category, September 2026 | | || --- | --- | --- || Maple Street Tool Library is fictional. Sample workbook for pdfconversionapi.com, CC0 1.0. | | || | | || Category | Loans | Deposits held || Power tools | 3 | 25 || Garden | 4 | 10 || Ladders | 2 | 0 || Woodworking | 3 | 60 || All categories | 12 | 95 |
- Input
- tool-library-loans.xlsx (11 KB)
- Output
- tool-library-loans.md (1.7 KB)
- Source
- A fictional tool library's September loans workbook (three sheets, one of them hidden, with formulas and a chart) 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 | Write ## Loans, ## Summary and so on above each table. With false the tables follow each other without headings. |
FirstRowIsHeaderboolean, default true | Use each sheet's first row as the table header. With false the header is the column letters (| A | B |) and the first row becomes data. |
WorksheetNamestring | Convert one sheet only, found without regard to case. WorksheetName=Loans gives a 1,305-byte file with just that table. |
WorksheetIndexinteger | One sheet by position from 1; hidden sheets are counted.Values: 1 to the number of sheets |
ActiveWorksheetOnlyboolean, default false | Convert only the tab that was open when the file was saved. The sample was saved on Summary, so the output is that one 345-byte table. |
Passwordstring | Open password for an encrypted workbook. The locked copy of the sample converted to the same 1,776 bytes with it; without it the API answers 422. |
Not supported yet
Setting one of these to anything but its default returns 400.
| Option | What to do instead |
|---|---|
CellValues | Only formatted is accepted; raw and other values return 400 "CellValues=raw is not supported yet for spreadsheets to Markdown". Despite the name, cells hold the stored values today (25, not $25.00). |
IncludeHiddenRows | true returns 400. Hidden rows are included anyway: the sample's hidden row L-1047 is in the Loans table. |
IncludeHiddenColumns | true returns 400; hidden columns, such as the sample's Shelf column, are already included. |
Images | Any value other than embed returns 400 "Images is not supported yet for XLSX → MD". Charts and pictures are not written either way. |
What carries over, and what doesn't
Kept
- Every sheet, in tab order, under a heading with its name: Loans, Summary and the hidden Deposits sheet
- Cell values as stored: 7 for the Days formula, 25 for the deposit, true and false for the Returned flags
- Dates in ISO 8601: the 9/1/2026 cell becomes 2026-09-01
- Pipes inside cells are escaped as
\|and line breaks become<br>, so a cell never breaks the table - Empty cells as empty table cells, so each row has as many cells as the header
Lost or not yet supported
- Number formats: currency, percent and thousands separators are not applied, so $25.00 is written as 25.
- Hidden rows and columns are not removed, even though
IncludeHiddenRowsandIncludeHiddenColumnsdefault to false. - Charts, pictures, comments, colors, borders and merged cells. A title row above a table becomes that table's header row, as on the sample's Summary sheet.
- Formulas: only their stored results are written.
Limits
- Requests up to 10 MB on the Free plan or 100 MB on paid plans; conversions that run longer than 120 seconds return 504.
- A file the reader doesn't recognize returns 422 "the input file could not be read: Cannot detect file format".
Where teams use it
Spreadsheets in a knowledge base or a docs repository
Markdown tables render on GitHub, in wikis and static site generators, and their diffs show which cells changed from one version to the next.
Context for a language model
A sheet name heading plus a plain table is easy for a model to read, and much smaller than the XLSX package: 1,776 bytes of text for a 10,832-byte workbook in the sample.
Pasting data into an issue or a pull request
Turn a workbook attached to a ticket into a table you can quote in the discussion.
Questions
Are hidden sheets included?
Yes. Every sheet is converted, the hidden ones too: the sample's hidden Deposits sheet is the third table. To leave sheets out, convert the ones you want with WorksheetName or WorksheetIndex, one request each.
Why does the Markdown show 25 when Excel shows $25.00?
Cells are written with the value stored in the file, not the text Excel displays, so number formats such as currency are not applied. Dates are the exception: they are written as ISO dates like 2026-09-01.
How are line breaks and pipe characters in cells handled?
A | inside a cell is written as \| and a line break as <br>, so the table structure stays intact. In our test, "left | right" became left \| right.
Can I get the table without the sheet headings?
Send SheetHeadings=false. Combine it with WorksheetName to get a single bare table you can paste anywhere.
Convert your first XLSX file in a minute.