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
curl -X POST https://api.txtfetch.com/v1/extract \
  -H "Authorization: Bearer $TXTFETCH_KEY" \
  -F file=@quarterly-board-deck.pptx
Python
import 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"])
JavaScript
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);
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("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
curl -X POST "https://api.txtfetch.com/v1/extract?url=https://example.com/decks/product-roadmap.pptx" \
  -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/decks/product-roadmap.pptx"},
)

print(r.json()["extracted_text"])
JavaScript
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);
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/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 →