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
curl -X POST https://api.txtfetch.com/v1/extract \
  -H "Authorization: Bearer $TXTFETCH_KEY" \
  -F file=@q1-revenue-workbook.xlsx
Python
import 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"])
JavaScript
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);
Go
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
curl -X POST "https://api.txtfetch.com/v1/extract?url=https://example.com/reports/regional-sales.xlsx" \
  -H "Authorization: Bearer $TXTFETCH_KEY"
Python
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"])
JavaScript
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);
Go
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)

fy2026-budget-forecast.xlsx · application/vnd.openxmlformats-officedocument.spreadsheetml.sheet · left pane read via Apache Tika, right pane: the human-checked expected.json

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	69100

See all ten documents, with the full explanation →

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 per sheet instead of space-separated cell values.

go-further

Send a real .xlsx through it.

One HTTP call returns the text. Read one in your browser first, for free.

Get an API key →

Open the free .xlsx reader →