XLS to XLS API
Post an .xls file to POST /v2/convert/xls/to/xls and get it back re-saved as an Excel 97-2003 workbook by a spreadsheet engine. Sent with Password, a protected file comes back without its password.
- Endpoint
- POST /v2/
convert/ xls/ to/ xls - Accepts
- xls
- Sample timing
- 0.597 s 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/xls \
-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/xls",
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/xls";
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.597,
"Files": [
{
"FileName": "van-fuel-log.xls",
"FileExt": "xls",
"FileSize": 10752,
"FileData": "0M8R4KGxGuEAAAAAAAAAAAAA…"
}
]
}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
- van-fuel-log.xls (11 KB)
- Output
- van-fuel-log.xls (11 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 |
|---|---|
Passwordstring | The open password of a protected .xls. Our test file, saved with a password in an office suite, came back as a 5,632-byte .xls that reads with no password; without the right one the answer is 422 "the input file is password-protected". |
FileNamestring | Base name for the returned .xls; the original name is used without it. |
What carries over, and what doesn't
Kept
- The same content, record for record: both sheets, all 22 formulas, 33 text cells, the merged title, the note, the drop-down list and the conditional format
- Number formats, fonts, column widths, page setup, headers and footers
- The selected sheet and the file's title and author
- The format itself: Excel 97-2003, 10,752 bytes in and 10,752 bytes out for the sample
Lost or not yet supported
- Note authors: "Office" was rewritten as "Unknown Author".
- Any open password, when you send
Password: the file is saved without encryption. - The bytes are not identical even where nothing changed, so a checksum of the file will differ.
Limits
- Only Excel's binary .xls format is opened; an HTML table or an XLSX with a .xls name returns 422 "the input file could not be read: it could not be opened as XLS".
- Sheet and page options are refused on this route, for example
Scalewith 400 "Scale is not supported yet for XLS files". - Requests up to 10 MB on the Free plan or 100 MB on paid plans; 120 seconds per conversion, then 504.
Where teams use it
Removing a known password before processing
The JSON and Markdown routes refuse a protected .xls even with its password. Send it here once with the password and keep the open copy in the same format.
Feeding a system that only accepts .xls
Unlike xls/to/xlsx, the answer stays in the 1997-2003 format, so the re-saved file can go wherever the original was going.
Questions
What actually changes when I re-save an .xls?
In the sample, almost nothing visible: the census of records inside the file (formulas, numbers, text, notes, merges, validation and conditional formats) matched before and after. The note's author name changed, the print titles gained an extra defined name (2 names before, 3 after), and the bytes differ.
How do I remove the password from an .xls?
Send the file with Password set to its password. The returned .xls has no encryption. This only works when you know the password; a wrong one returns 422.
Should I convert to XLSX instead?
If the next step can read XLSX, yes: /v2/convert/xls/to/xlsx keeps the same content in the current format, which more libraries read. Keep .xls when another system requires it.
Convert your first XLS file in a minute.