https://txtfetch.com/extract/xlsx/
Spreadsheets, resolved not raw.
Cell values are indices into a shared string table. Formulas store their formula, not their answer. Tika resolves both before you ever see the response.
the-problem
A spreadsheet's cells don't hold their own text. In .xlsx, string cell values are indices into xl/sharedStrings.xml, a dedup table shared across the whole workbook, not inline text. So reading cells in isolation just gets you numbers pointing at other numbers. Formula cells store the formula itself, like =SUM(A1:A10). The number a human sees is a separately cached result value, and plenty of naive readers grab the wrong one, or the formula string, instead. Number formats add another layer of indirection. A cell can be stored as 0.42 and displayed as 42%. Or it can be stored as a serial integer and displayed as a date. So 'the value' and 'what's on screen' genuinely differ. Hidden sheets, hidden rows, and merged cells still carry meaning that a flat text dump can drop entirely.
one-request-solution
txtfetch resolves the indirection for you. Shared-string references become their actual text, and formula cells return Tika's cached computed value rather than the =SUM() source. That means the numbers in your response match what a person looking at the sheet would see. For table structure, with rows and columns kept intact instead of flattened into a wall of numbers, request ?format=markdown.
curl -X POST https://api.txtfetch.com/v1/extract \
-H "Authorization: Bearer $TXTFETCH_KEY" \
-F file=@q1-revenue-workbook.xlsximport os
import requests
with open("q1-revenue-workbook.xlsx", "rb") as f:
r = requests.post(
"https://api.txtfetch.com/v1/extract",
headers={"Authorization": f"Bearer {os.environ['TXTFETCH_KEY']}"},
files={"file": f},
)
print(r.json()["extracted_text"])import { readFile } from "node:fs/promises";
const file = new Blob([await readFile("q1-revenue-workbook.xlsx")]);
const form = new FormData();
form.append("file", file, "q1-revenue-workbook.xlsx");
const res = await fetch("https://api.txtfetch.com/v1/extract", {
method: "POST",
headers: { Authorization: `Bearer ${process.env.TXTFETCH_KEY}` },
body: form,
});
const { extracted_text } = await res.json();
console.log(extracted_text);package main
import (
"bytes"
"encoding/json"
"fmt"
"io"
"mime/multipart"
"net/http"
"os"
)
type extractResponse struct {
Status string `json:"status"`
ExtractedText string `json:"extracted_text"`
}
func main() {
f, err := os.Open("q1-revenue-workbook.xlsx")
if err != nil {
panic(err)
}
defer f.Close()
var body bytes.Buffer
writer := multipart.NewWriter(&body)
part, err := writer.CreateFormFile("file", "q1-revenue-workbook.xlsx")
if err != nil {
panic(err)
}
if _, err := io.Copy(part, f); err != nil {
panic(err)
}
writer.Close()
req, err := http.NewRequest("POST", "https://api.txtfetch.com/v1/extract", &body)
if err != nil {
panic(err)
}
req.Header.Set("Authorization", "Bearer "+os.Getenv("TXTFETCH_KEY"))
req.Header.Set("Content-Type", writer.FormDataContentType())
resp, err := http.DefaultClient.Do(req)
if err != nil {
panic(err)
}
defer resp.Body.Close()
var result extractResponse
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
panic(err)
}
fmt.Println(result.ExtractedText)
}Or skip the download. Pass a url parameter and txtfetch fetches the document server-side:
curl -X POST "https://api.txtfetch.com/v1/extract?url=https://example.com/reports/regional-sales.xlsx" \
-H "Authorization: Bearer $TXTFETCH_KEY"import os
import requests
r = requests.post(
"https://api.txtfetch.com/v1/extract",
headers={"Authorization": f"Bearer {os.environ['TXTFETCH_KEY']}"},
params={"url": "https://example.com/reports/regional-sales.xlsx"},
)
print(r.json()["extracted_text"])const endpoint = new URL("https://api.txtfetch.com/v1/extract");
endpoint.searchParams.set("url", "https://example.com/reports/regional-sales.xlsx");
const res = await fetch(endpoint, {
method: "POST",
headers: { Authorization: `Bearer ${process.env.TXTFETCH_KEY}` },
});
const { extracted_text } = await res.json();
console.log(extracted_text);package main
import (
"encoding/json"
"fmt"
"net/http"
"net/url"
"os"
)
type extractResponse struct {
Status string `json:"status"`
ExtractedText string `json:"extracted_text"`
}
func main() {
endpoint, err := url.Parse("https://api.txtfetch.com/v1/extract")
if err != nil {
panic(err)
}
q := endpoint.Query()
q.Set("url", "https://example.com/reports/regional-sales.xlsx")
endpoint.RawQuery = q.Encode()
req, err := http.NewRequest("POST", endpoint.String(), nil)
if err != nil {
panic(err)
}
req.Header.Set("Authorization", "Bearer "+os.Getenv("TXTFETCH_KEY"))
resp, err := http.DefaultClient.Do(req)
if err != nil {
panic(err)
}
defer resp.Body.Close()
var result extractResponse
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
panic(err)
}
fmt.Println(result.ExtractedText)
}{
"status": "success",
"extracted_text": "..."
}what-comes-back
That is the shape. This is the text. A real .xlsx from the benchmark corpus, word-diffed against the raw parser output. Nothing here was written for this page. Every character comes from a committed fixture.
FY2026 Budget Forecast (XLSX)
raw parser output
Opex
Line item Q1 Q2
Cloud hosting 4200 4600
Payroll 61000 64500
Summary
Total 65200 69100
the human-checked ideal
Opex
Line item Q1 Q2
Cloud hosting 4200 4600
Payroll 61000 64500
Summary
Total 65200 69100See all ten documents, with the full explanation →
try-your-own
Drop your own .xlsx file below. Your browser reads it. Nothing uploads, and txtfetch never sees it.
Stays in your browser. Up to 25 MB.
What's not in this text:
Your browser read this file. No request left your browser, so txtfetch never received it. The API returns this same text as { status, extracted_text }.
See the real API response in the playground. Need many files, a folder, or a .zip? Use file-to-text instead.
formats-covered
.xlsx.xls.xlsm.csv.ods
response-options
Need rows and columns, not a flat wall of numbers? ?format=markdown returns each sheet as a proper markdown table.
faq
- Does extraction return the formula or the calculated value?
- The calculated value. Tika reads the cached formula result that Excel stores alongside the formula, so you get 18200 instead of the literal string =SUM(A1:A10).
- Do percentages and dates come back as raw numbers?
- No. The cell's number format is applied before the text is returned. A cell stored as 0.42 with a percent format comes back as 42%. A date stored as the serial number 46229 comes back as 2026-07-26. A cell with no format applied returns its stored value unchanged, which is what you want for a plain number.
- Are hidden sheets and hidden rows included?
- Yes. txtfetch extracts hidden sheets and rows along with visible ones, since hiding a sheet in Excel doesn't remove its data.
- Can I get real table structure instead of a flat text dump?
- Yes. Pass ?format=markdown, and rows and columns come back as a markdown table under a heading for each sheet. The default text output is tab-separated, one line per row. A merged cell keeps its column, with an empty cell for each covered one. See /docs/tables for the raw output.
go-further
- Read the .xlsx guide →
- Does a table come back as a table? Real output by format →
- Does the text in a picture come back? Real output by format →
- Drop a real .xlsx and see the exact text every sheet extracts to, free →
- The wider Office-family parsing story (DOCX/PPTX/XLSX) →
- Charset detection for plain .csv and other delimited exports →
- API quickstart →
- How the pipeline works →
- Extract it from your language →
- Get an API key →
Send a real .xlsx through it.
One HTTP call returns the text. Read one in your browser first, for free.
Get an API key →