XLSX to CSV API
Send a workbook to POST /v2/convert/xlsx/to/csv and one worksheet comes back as CSV. The converter reads the values saved in the file, so no copy of Excel or another spreadsheet app runs.
- Endpoint
- POST /v2/
convert/ xlsx/ to/ csv - Accepts
- xlsx, xls, xlsb, ods
- 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/csv \
-H "Authorization: Bearer $PDFCONVERSIONAPI_KEY" \
-F File=@price-list.xlsx \
--form-string WorksheetName=Prices \
-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/csv",
headers={"Authorization": f"Bearer {key}"},
files=[("File", open("price-list.xlsx", "rb"))],
data={
"WorksheetName": "Prices",
},
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 = "price-list.xlsx";
form.append("File", new Blob([await readFile(file)]), file);
form.append("WorksheetName", "Prices");
const url = "https://api.pdfconversionapi.com/v2/convert/xlsx/to/csv";
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": "price-list.csv",
"FileExt": "csv",
"FileSize": 661,
"FileData": "c2t1LGRlc2NyaXB0aW9uLHVu…"
}
]
}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.
price-list.csv, 9 lines
sku,description,unit_cost,markup,list_price,on_hand,last_counted,discontinued00412,"Galvanized deck screws, 1 lb box",6.85,0.4,9.59,140,2026-10-27,false00413,"Galvanized deck screws, 5 lb box",27.1,0.35,36.59,38,2026-10-27,false10220,"Utility knife, 18 mm ""snap-off"" blade",4.2,0.6,6.72,85,2026-10-28,false10221,"Replacement blades, 10 pack",3.15,0.6,5.04,210,2026-10-28,false20517,"Exterior latex paint, satin, 1 gal",21.4,0.45,31.03,64,2026-10-29,false20518,"Exterior latex paint, satin, 5 gal",98,0.4,137.2,9,2026-10-29,false31002,"Brass hose bib, 1/2""",11.75,0.5,17.63,22,2026-10-30,true40090,"Leaf rake, 24""",9.6,0.55,14.88,47,2026-10-30,false
- Input
- price-list.xlsx (8.6 KB)
- Output
- price-list.csv (661 bytes)
- Source
- Written for this site (fictional company); XLSX saved by a spreadsheet app (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 |
|---|---|
WorksheetNamestring | Export the tab with this name; upper and lower case are treated alike. A name the workbook doesn't have returns 400 "no worksheet named 'Inventory'". |
WorksheetIndexinteger | Export the tab at this position, counting from 1 in tab order. Hidden sheets count. Don't send it together with WorksheetName: the pair returns 400 "send WorksheetName or WorksheetIndex, not both".Values: 1 to the number of sheets |
ActiveWorksheetOnlyboolean, default false | With true, export the tab that was open when the file was saved instead of the first one: a test workbook saved with its second tab open exported that tab. WorksheetName or WorksheetIndex wins when sent with it. It also works on xlsb/to/csv: our two-tab XLSB test file, saved with its Daily tab open, exported Daily. On ods/to/csv it returns 400 for now. |
Passwordstring | Opens a workbook saved with a password to open, as Excel 2010 and later encrypt it: a protected copy of the sample gave the same 661-byte CSV. Protected .xls files can't be opened on this route yet (400 with a password, 422 without). Sent with a workbook that has no password, it changes nothing. |
Delimiterstring, default , | Separator written between fields. A field that contains it, a double quote or a line break is wrapped in double quotes.Values: One ASCII character, or tab |
FileNamestring | Name for the CSV file, without the extension. Without it the CSV takes the uploaded file's name. |
StoreFileboolean, default false | Return a download link instead of the file. The link expires after 3 hours and the file is then deleted. |
What carries over, and what doesn't
Kept
- The value saved in every cell, including each formula's last result (list_price in the sample is a ROUND formula)
- Dates as ISO 8601: 10/27/2026 on screen becomes 2026-10-27, and a date with a time becomes 2026-10-30 16:45:00
- Numbers without their display format, ready to parse: $6.85 is written as 6.85
- Text exactly as stored, including leading zeros in text cells such as the SKU 00412
- Commas, double quotes and line breaks inside cells, quoted by the usual CSV rules
- The same output from XLS and ODS copies of the sample workbook; we compared the three CSV files byte for byte
Lost or not yet supported
- Display formats. 35% is written as 0.35, the $ sign goes, and 27.10 becomes 27.1. For comparison, an office suite's CSV export of the same file writes 35% and 10/27/2026.
- Formulas are not recalculated. A workbook saved by a library that stores no formula results (we tried one made with openpyxl 3.1.5) gives no value for those cells.
TRUEandFALSEbecometrueandfalse, and error values are spelled differently: #DIV/0! becomes #Div0 and #N/A becomes #NA.- A cell holding only a time of day gets the date 1899-12-31 in front of it: 08:30 becomes 1899-12-31 08:30:00.
- Empty rows above and empty columns left of the first filled cell are dropped. A table that starts at B3 starts on the first line of the CSV.
- Hidden rows and hidden sheets are exported like visible ones. If the first tab is hidden, a request with no sheet chosen exports that tab.
- In a merged range, the value goes in the first column and the other columns are empty.
Limits
- One worksheet per request.
- A password-protected workbook returns 422 "the input file is password-protected" until you send the right
Password. - Requests up to 10 MB on the Free plan or 100 MB on paid plans, all files and fields together.
- A conversion can run for up to 120 seconds; after that the API returns 504.
Where teams use it
Importing spreadsheets your users upload
Customers send .xlsx files; your importer already parses CSV. Convert on upload and keep one parser. Dates arrive as 2026-10-27 whatever the regional settings of the person who saved the file.
Reading one tab from a multi-tab workbook
The sample price list has a Prices tab, a Suppliers tab and an About tab. Ask for WorksheetName=Prices and you get that table alone, header row first.
Loading spreadsheets into scripts and warehouses
Command-line tools, COPY statements and dataframe libraries all read CSV. Converting first means the job never needs a spreadsheet library that reads XLSB or ODS.
Questions
Which worksheet do I get if I don't choose one?
The first tab in the workbook, even if another tab was open when the file was saved and even if the first tab is hidden. Pass WorksheetName, or WorksheetIndex counting from 1, to pick another, or ActiveWorksheetOnly=true to get the tab that was open.
How do I export every sheet in a workbook?
Send one request per sheet. If you don't know how many there are, step WorksheetIndex up from 1; past the last tab the API returns 400 "WorksheetIndex is 4, but the workbook has 3 worksheets", which also gives you the count.
Why is 35% in my spreadsheet 0.35 in the CSV?
The CSV carries the stored value, not the text the cell shows. Numbers come out with no $ signs or thousands separators to strip, but percentages and custom formats are not applied. Dates are the exception: they are written as ISO 8601 text.
Are formulas recalculated before export?
No. The converter reads the result saved with each formula, which the spreadsheet app that saved our sample had stored. If your files come from a library that writes formulas without results, compute the values before writing the file, or the CSV gets nothing for those cells.
Can I send .xls, .xlsb or .ods files?
Yes. /v2/convert/xls/to/csv, /v2/convert/xlsb/to/csv and /v2/convert/ods/to/csv run the same converter with the same parameters, with two gaps for now: ActiveWorksheetOnly=true returns 400 for ODS files, and a password-protected .xls can't be opened. The reader detects the format from the file's bytes, so an ODS file sent to the xlsx route converts too.
Convert your first XLSX file in a minute.