> ## Documentation Index
> Fetch the complete documentation index at: https://docs.markpdf.tech/llms.txt
> Use this file to discover all available pages before exploring further.

# POST /pdf/index

> Compact structural index of a PDF for AI agents. Returns sections, headings and metadata without converting the entire document.

`/pdf/index` returns a **compact map** of the PDF (\~5-15KB JSON) without extracting Markdown. Intended for **AI agents / RAG** who need to navigate large documents without paying the cost (tokens + time) of converting the entire document.

The correct flow is:

1. Client calls `/pdf/index` -> receives the spine with `sections[]` and `pages[]`.
2. Client/agent decides which pages interest them.
3. Client calls `/convert/from-url` with `pages="47-58"` to obtain Markdown **only from those pages**.

Result: **a 500-page PDF is navigated with 5KB of spine + 30KB of the requested chunk**, instead of 10MB of full Markdown. The IA agent pays 20-30x fewer tokens to LLM, and the backend pulls 400x fewer pages.

## When to use

<Tip>
  * IA agent / RAG that searches for specific information in large PDFs.
  * Legal / academic / financial documents with well-defined sections.
  * "Preview" type UI where the user navigates before downloading.
  * Pre-vavalidation of PDF (page count, format) without conversion cost.
</Tip>

## When NOT to use

<Warning>
  * Small PDFs (\<10 pages): the fixed cost of the spine is not worth it, use `/convert/raw` or direct `/convert/from-url`.
  * Client who DOES want the complete Markdown: skip the index and request `/convert/from-url` without `pages`.
</Warning>

\##Request

```bash theme={null}
POST /pdf/index
Content-Type: application/json

{
  "url": "https://storage.example.com/document.pdf?signature=...",
  "filename": "document.pdf"
}
```

<ParamField body="url" type="string" required>
  URL GET presigned from PDF in your storage (S3, R2, Supabase, GCS, Azure Blob).
</ParamField>

<ParamField body="filename" type="string" default="document.pdf">
  Logical name only for logs.
</ParamField>

## Response

```json theme={null}
{
  "ok": true,
  "filename": "document.pdf",
  "content_hash": "sha256...",
  "spine": {
    "page_count": 523,
    "input_bytes": 524288000,
    "font_model": {
      "body_size": 10.5,
      "heading_sizes": [16.0, 13.0, 11.5]
    },
    "sections": [
      {"page": 1, "level": 1, "text": "1. Executive Summary"},
      {"page": 12, "level": 1, "text": "2. Methodology"},
      {"page": 47, "level": 1, "text": "3. Results"},
      {"page": 58, "level": 2, "text": "3.1 Quarterly Performance"}
    ],
    "repeated_headers_footers": [
      "Acme Corp Confidential",
      "Q4 2025 Annual Report"
    ],
    "pages": [
      {"page": 1, "chars": 1842, "first_line": "Q4 2025 Annual Report", "headings": [...]},
      {"page": 2, "chars": 2103, "first_line": "Table of contents", "headings": []}
    ],
    "pages_truncated": true,
    "estimated_tokens_full": 256000,
    "estimated_tokens_spine_only": 4200,
    "how_to_fetch_section": {
      "endpoint": "POST /convert/from-url",
      "params": {"url": "<same input_url>", "pages": "<start>-<end>"},
      "tip": "Mira sections[].page para saber donde empieza cada seccion. pages='47-57' extrae solo ese rango."
    }
  },
  "timings": {
    "spine_ms": 412,
    "total_request_ms": 412
  }
}
```

## Spine fields

| Field                         | Meaning                                                                                             |
| ----------------------------- | --------------------------------------------------------------------------------------------------- |
| `page_count`                  | Total number of pages.                                                                              |
| `input_bytes`                 | Size of the downloaded PDF.                                                                         |
| `font_model.body_size`        | Body font size (median over sample).                                                                |
| `font_model.heading_sizes`    | Candidates for heading sizes (ordered descending).                                                  |
| `sections[]`                  | List of detected headings: page + level (1-3) + text. Cap 500.                                      |
| `repeated_headers_footers`    | Repeated headers/footers on many pages: noise to ignore when chunking.                              |
| `pages[]`                     | Light metadata per page: chars, first line, local headings. Truncated to 200 entries in giant docs. |
| `pages_truncated`             | `true` if `page_count > 200`; use `sections[]` to navigate the rest.                                |
| `estimated_tokens_full`       | Estimation of tokens from the complete Markdown (rude).                                             |
| `estimated_tokens_spine_only` | Tokens of the spine itself (to compare with the cost of ordering sections).                         |
| `how_to_fetch_section`        | Reminder on how to request the actual content with `pages=`.                                        |

## Full flow example (IA agent)

```python theme={null}
import httpx

API = "https://api.markpdf.tech"
PDF = "https://bucket.example.com/research.pdf?sig=..."
HEADERS = {"x-api-key": "TU_KEY"}

# 1. Index the PDF
r = httpx.post(f"{API}/pdf/index", json={"url": PDF}, headers=HEADERS)
spine = r.json()["spine"]
print(f"PDF tiene {spine['page_count']} papages")
print(f"Tokens estimados completo: {spine['estimated_tokens_full']}")
print(f"Tokens del spine: {spine['estimated_tokens_spine_only']}")

# 2. Agent decides: he only wants "Results"
target = next(s for s in spine["sections"] if "Results" in s["text"])
next_sec = next((s for s in spine["sections"] if s["page"] > target["page"]), None)
end = (next_sec["page"] - 1) if next_sec else spine["page_count"]
pages = f"{target['page']}-{end}"

# 3. Order SOLO that range
r = httpx.post(
    f"{API}/convert/from-url",
    json={"url": PDF, "pages": pages, "mode": "fast"},
    headers=HEADERS,
)
markdown_results = r.text
```

## Cost and performance

* **Typical latency**: 300-700ms for PDFs up to 512MB (sample of 32 pages + head of each one).
* **Server cost**: constant with respect to the size of PDF (does not scale with pages).
* **AI client cost**: spine \~1.5K-4K tokens vs full Markdown 50K-300K tokens.

| Case                                          | PDF  |                Without index |                                With index |
| --------------------------------------------- | ---- | ---------------------------: | ----------------------------------------: |
| 500 pages, agent wants 1 section (\~12 pages) | 10MB | 250K tokens, \~5s extraction |             **15K tokens, \~0.7s mining** |
| 1000 pages, agent wants 3 sections            | 30MB |                  600K tokens |                            **20K tokens** |
| 50 pages, agent wants everything              | 1MB  |                   25K tokens | index does not contribute (do not use it) |

## Limitations v1

* `pages[]` truncated to 200 entries (configurable via `PDF_SPINE_MAX_PAGE_ENTRIES`). For documents >200 pages, use `sections[]` as a navigation map.
* Table detection not included v1. Only headings + body chars.
  -Without server cache: the client keeps the spine. Repeat `/pdf/index` re-download and re-process.

## Security

* Same anti-SSRF as `/convert/from-url`: only HTTPS, public hosts.
* PDF header vavalidation and `MAX_PDF_PAGES` enforce.
* API key + credit (`tier=index`, fixed cost 1 credit per call).
* No PDFs or spine are saved on the server.

See also: [POST /convert/from-url](/docs/api/convert-from-url), [Modes](/docs/concepts/modes), [Compression](/docs/concepts/compression).
