XLSX to JSON API
Post a workbook to POST /v2/convert/xlsx/to/json and get one worksheet back as a JSON array: an object per row, its keys taken from the first row. The sample's Loans sheet becomes 12 objects with dates, amounts and TRUE/FALSE flags typed.
- Endpoint
- POST /v2/
convert/ xlsx/ to/ json - 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/json \
-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/json",
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/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": "tool-library-loans.json",
"FileExt": "json",
"FileSize": 2902,
"FileData": "WwogIHsKICAgICJDYXRlZ29y…"
}
]
}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.json, first 30 of 146 lines
[ { "Category": "Power tools", "Days": 7.0, "Deposit": 25.0, "Due": "2026-09-08", "Item": "Cordless drill", "Loan": "L-1041", "Member": "M-208", "Out": "2026-09-01", "Returned": true, "Shelf": "B2" }, { "Category": "Garden", "Days": 7.0, "Deposit": 10.0, "Due": "2026-09-09", "Item": "Hedge trimmer", "Loan": "L-1042", "Member": "M-114", "Out": "2026-09-02", "Returned": true, "Shelf": "D4" }, { "Category": "Ladders", "Days": 3.0, "Deposit": 15.0, "Due": "2026-09-06",
- Input
- tool-library-loans.xlsx (11 KB)
- Output
- tool-library-loans.json (2.8 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 |
|---|---|
WorksheetNamestring | The sheet to read, matched without regard to case (summary finds Summary). Without a sheet option the first sheet is read, whichever tab was open. An unknown name returns 400 with "no worksheet named 'Nope'". |
WorksheetIndexinteger | The sheet's position, from 1, counting hidden sheets: 3 reads the sample's hidden Deposits sheet. For this 3-sheet file, 4 returns 400 with "WorksheetIndex is 4, but the workbook has 3 worksheets".Values: 1 to the number of sheets |
ActiveWorksheetOnlyboolean, default false | Read the tab that was open when the workbook was saved instead of the first one. For the sample that is Summary. |
FirstRowIsHeaderboolean, default true | With false, every row, the header included, becomes an array of values in column order, and the file shrinks from 2,902 to 1,938 bytes. Anything other than true or false returns 400.Values: true, false |
Passwordstring | Opens a workbook encrypted with a password to open. The locked copy of the sample gave the same 2,902-byte JSON with it and 422 "the input file is password-protected" without it. |
FileNamestring | Name of the JSON file: FileName=september returns september.json. |
What carries over, and what doesn't
Kept
- Every row of the chosen sheet below the header: 12 objects for the sample's Loans sheet, L-1041 to L-1052
- Number cells as JSON numbers and TRUE/FALSE cells as JSON booleans
- Dates as ISO 8601 text: the cell that shows 9/1/2026 arrives as "2026-09-01"
- Formula results as stored in the file:
Days(=F2-E2) is 7.0 andDeposit, looked up on the hidden Deposits sheet, is 25.0 - Empty cells as null, and a key for every column, so all objects have the same keys
Lost or not yet supported
- Column order: keys inside each object are sorted alphabetically, so
Categorycomes beforeLoan. UseFirstRowIsHeader=falseto keep the order as arrays. - Number formats: $25.00 arrives as 25.0. XLSX stores every number as a decimal, so whole numbers keep a .0.
- Hidden rows and columns are not filtered out: loan L-1047 (a hidden row) and the hidden Shelf column are both in the sample's output.
- Formulas themselves, charts, styles and the other sheets. Only one sheet is read per request.
- A formula saved without a result comes out as null. Re-save the workbook with
/v2/convert/xlsx/to/xlsxfirst: in our test the four null totals became 260, 150, 54 and 464.
Limits
- One sheet per request; run one request per sheet to export several.
- Requests up to 10 MB on the Free plan or 100 MB on paid plans. A conversion that takes more than 120 seconds returns 504.
- A file that isn't a spreadsheet returns 422 "the input file could not be read: Cannot detect file format".
Where teams use it
Importing a spreadsheet someone emailed into your database
Read the rows as objects, validate each field, and insert them. Typed values save a parsing step: amounts arrive as numbers, flags as true or false, dates as 2026-09-01.
Giving a language model or a search index the data, not the layout
One JSON object per row is a compact, unambiguous form for prompts and embeddings; there are no merged cells or print settings to explain.
Checking an exported report in tests
Convert the workbook your code generated and assert on the JSON, without a spreadsheet library in the test suite.
Questions
Why are the keys in alphabetical order instead of my column order?
Each row is written as a JSON object, and the objects are written with sorted keys. JSON readers don't depend on key order, but if your code does, send FirstRowIsHeader=false: rows then come as arrays in column order, with the header as the first array.
My sheet has a title above the headers. What do the keys look like?
The first row of the sheet's used range is taken as the header. In the sample's Summary sheet that row holds the title, so the keys became "Loans by category, September 2026", "column2" and "column3", and the real headers turned into a data row. Empty header cells are named column1, column2 and so on. Move the title out of the sheet, or read the data with FirstRowIsHeader=false and pick the header row yourself.
Are dates and times converted?
Yes. Cells with a date format become text in ISO 8601: "2026-09-01" for a date, and "2026-03-02 13:00:00" when there is a time of day. Midnight is written as the date alone.
Which sheet is read?
The first one in tab order, even if another tab was open when the file was saved. Choose another with WorksheetName, WorksheetIndex or ActiveWorksheetOnly=true.
Can I send an XLS or XLSB file to this route?
The reader recognizes the format from the file's content, so an XLS sent to xlsx/to/json was read correctly in our test. Use the route named after your format anyway: /v2/convert/xls/to/json and /v2/convert/xlsb/to/json give the same output.
Convert your first XLSX file in a minute.