Slide text and speaker notes, together.
Most extractors keep one or the other. txtfetch reads slide bodies and speaker notes from the same request.
the-problem
A .pptx is an OOXML zip archive: each slide's visible text lives in ppt/slides/slideN.xml, but speaker notes are a separate part entirely — ppt/notesSlides/ — with its own XML files and its own relationship IDs back to the slide they annotate. Extractors that read slideN.xml and stop miss the notes silently; extractors built around the notes miss the body. Text also hides in grouped shapes (nested inside other shapes, not top-level), SmartArt diagrams (their text sits in diagrams/data*.xml, disconnected from the visible diagram layout), tables, and text baked into slide images as pixels rather than markup. Slide order compounds all of this — it's defined by relationships in presentation.xml, not by the numeric order slide files happen to be zipped in.
one-request-solution
txtfetch's Office parser reads both parts of a .pptx in one pass: slide body text from ppt/slides/, speaker notes from ppt/notesSlides/, and grouped-shape and SmartArt text pulled out of their nested XML — all folded into one extracted_text response in the deck's real slide order.
curl -X POST https://api.txtfetch.com/v1/extract \
-H "Authorization: Bearer $TXTFETCH_KEY" \
-F file=@quarterly-board-deck.pptximport os
import requests
with open("quarterly-board-deck.pptx", "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("quarterly-board-deck.pptx")]);
const form = new FormData();
form.append("file", file, "quarterly-board-deck.pptx");
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("quarterly-board-deck.pptx")
if err != nil {
panic(err)
}
defer f.Close()
var body bytes.Buffer
writer := multipart.NewWriter(&body)
part, err := writer.CreateFormFile("file", "quarterly-board-deck.pptx")
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/decks/product-roadmap.pptx" \
-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/decks/product-roadmap.pptx"},
)
print(r.json()["extracted_text"])const endpoint = new URL("https://api.txtfetch.com/v1/extract");
endpoint.searchParams.set("url", "https://example.com/decks/product-roadmap.pptx");
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/decks/product-roadmap.pptx")
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": "..."
}formats-covered
.pptx.ppt.pptm.potx.odp
response-options
Presenting slide structure to an LLM? ?format=markdown returns headings and bullet hierarchy per slide instead of one flat block of text.
faq
- Does extraction include PowerPoint speaker notes, or just slide text?
- Both. Speaker notes live in a separate ppt/notesSlides/ part of the .pptx archive from the slide body — txtfetch reads both parts and includes them together in the response.
- What about text inside grouped shapes or SmartArt diagrams?
- Yes — grouped shapes nest inside other shapes rather than sitting at the top level, and SmartArt text lives in its own diagrams/data*.xml part. Both are walked and included, not just top-level slide text boxes.
- Does it preserve the actual slide order?
- Yes — slide order comes from presentation.xml's relationship list, not from the filename order of the slideN.xml parts inside the zip, which don't reliably match reading order.
- Does it handle the legacy .ppt binary format too?
- Yes, and .pptm (macro-enabled) and .potx (template) — all four route through the same endpoint and return the same response shape.
go-further
Stop parsing. Start shipping.
Create an account and get an API key in minutes — the free Hobby plan needs no card.
Get started →