XLS to JSON API
Send an Excel 97-2003 file to POST /v2/convert/xls/to/json and get a sheet's rows as JSON objects named by its header row. The sample's Fuel sheet gives 11 objects: ten fill-ups and the Total row.
- Endpoint
- POST /v2/
convert/ xls/ to/ json - 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/json \
-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/json",
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/json";
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.json",
"FileExt": "json",
"FileSize": 2367,
"FileData": "WwogIHsKICAgICJDb3N0Ijog…"
}
]
}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.json, first 30 of 112 lines
[ { "Cost": 52.3838, "Date": "2026-06-01", "Gallons": 14.2, "Miles per gallon": 16.619718309859156, "Notes": null, "Odometer": 48210, "Price per gallon": 3.689, "Van": "Van 1" }, { "Cost": 61.9752, "Date": "2026-06-02", "Gallons": 16.8, "Miles per gallon": 15.595238095238095, "Notes": null, "Odometer": 61377, "Price per gallon": 3.689, "Van": "Van 2" }, { "Cost": 46.4875, "Date": "2026-06-04", "Gallons": 12.5, "Miles per gallon": 18.24, "Notes": null, "Odometer": 30554, "Price per gallon": 3.719, "Van": "Van 3"
- Input
- van-fuel-log.xls (11 KB)
- Output
- van-fuel-log.json (2.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 |
|---|---|
WorksheetNamestring | Sheet to export, in any letter case (vans works). Left out, the first sheet in tab order is read. |
WorksheetIndexinteger | Sheet position from 1. The sample has two sheets, so 3 returns 400 with "WorksheetIndex is 3, but the workbook has 2 worksheets".Values: 1 to the number of sheets |
ActiveWorksheetOnlyboolean, default false | Read the sheet that was selected when the file was saved, taken from the workbook's window settings. In a test file saved on its second sheet, that sheet was read. |
FirstRowIsHeaderboolean, default true | With false, each row becomes an array in column order and the header row is the first array; the sample's output then drops from 2,367 to 1,457 bytes. |
FileNamestring | Name of the JSON file, without the extension. |
What carries over, and what doesn't
Kept
- Number types as the .xls stores them: whole odometer readings such as 48210 and 13 gallons arrive as JSON integers, 14.2 gallons as a decimal
- Dates as ISO 8601 text: 06/01/2026 in the sheet becomes "2026-06-01"
- The line break inside the two-line note, as \n in the string
- The Total row, as an object whose Date is "Total" and whose empty cells are null
- Formula results stored in the file: Cost is 52.3838 for 14.2 gallons at $3.689
Lost or not yet supported
- Display formats: a miles-per-gallon cell shown as 16.6 arrives with every digit, 16.619718309859156, and $52.38 arrives as 52.3838. Round in your code if you need the shown values.
- Column order inside each object: keys come sorted alphabetically (Cost, Date, Gallons, ...).
- The cell note, the drop-down list, the conditional highlight and the second sheet; one sheet is read per request.
Limits
- Password-protected .xls files can't be read here: with the password the API returns 400 "password-protected XLS files can't be converted to JSON yet", without it 422. Re-save them with
/v2/convert/xls/to/xlsxandPasswordfirst, then convert the XLSX. - Only Excel's binary format is read. A web page saved as .xls returns 422 "the input file could not be read: Cannot detect file format".
- 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
Retiring a legacy export
When a supplier or an older system sends .xls attachments, convert each one to JSON as it arrives, and the rest of your code never has to read the binary format.
Loading historical data into a database
Archives of .xls workbooks from past years can be read sheet by sheet into rows for SQL or a data warehouse.
Questions
Why do some numbers have decimals and others don't?
An .xls file stores whole numbers in a compact integer form, and those come out as JSON integers (48210). Numbers with a fraction come out as decimals (14.2). Values are not rounded to the cell's display format.
What if the sheet starts with a title row?
The first row is used as the header, whatever it holds. The sample's Vans sheet starts with a merged title, so its keys became "Sunrise Rye Bakery: delivery vans" and column2 to column4. Use FirstRowIsHeader=false and choose the header row yourself, or move the title out of the sheet.
How do I convert every sheet?
Make one request per sheet with WorksheetIndex=1, 2 and so on. To see all sheets at once as text, /v2/convert/xls/to/md writes every sheet in one file.
Convert your first XLS file in a minute.