> ## 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

> Indice estructural compacto de un PDF para agentes IA. Devuelve secciones, headings y metadata sin convertir el documento entero.

`/pdf/index` devuelve un **mapa compacto** del PDF (\~5-15KB JSON) sin extraer Markdown. Pensado para **agentes IA / RAG** que necesitan navegar documentos grandes sin pagar el coste (tokens + tiempo) de convertirlo entero.

El flujo correcto es:

1. Cliente llama `/pdf/index` -> recibe el spine con `sections[]` y `pages[]`.
2. Cliente / agente decide que paginas le interesan.
3. Cliente llama `/convert/from-url` con `pages="47-58"` para obtener Markdown **solo de esas paginas**.

Resultado: **un PDF de 500 paginas se navega con 5KB de spine + 30KB del trozo pedido**, en vez de 10MB de Markdown completo. El agente IA paga 20-30x menos tokens al LLM, y el backend extrae 400x menos paginas.

## Cuando usarlo

<Tip>
  * Agente IA / RAG que busca informacion concreta en PDFs grandes.
  * Documentos legales / academicos / financieros con secciones bien delimitadas.
  * UI tipo "vista previa" donde el usuario navega antes de descargar.
  * Pre-validacion de PDF (page count, formato) sin coste de conversion.
</Tip>

## Cuando NO usarlo

<Warning>
  * PDFs pequenos (\<10 paginas): el coste fijo del spine no compensa, usa `/convert/raw` o `/convert/from-url` directo.
  * Cliente que SI quiere el Markdown completo: salta el indice y pide `/convert/from-url` sin `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 prefirmada del PDF en tu storage (S3, R2, Supabase, GCS, Azure Blob).
</ParamField>

<ParamField body="filename" type="string" default="document.pdf">
  Nombre logico solo para 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
  }
}
```

## Campos del spine

| Campo                         | Significado                                                                                                   |
| ----------------------------- | ------------------------------------------------------------------------------------------------------------- |
| `page_count`                  | Numero total de paginas.                                                                                      |
| `input_bytes`                 | Tamano del PDF descargado.                                                                                    |
| `font_model.body_size`        | Tamano de fuente del cuerpo (mediana sobre sample).                                                           |
| `font_model.heading_sizes`    | Candidatos a tamanos de heading (ordenados desc).                                                             |
| `sections[]`                  | Lista de headings detectados: pagina + nivel (1-3) + texto. Tope 500.                                         |
| `repeated_headers_footers`    | Cabeceras/pies repetidos en muchas paginas: ruido a ignorar al chunkear.                                      |
| `pages[]`                     | Metadata ligera por pagina: chars, primera linea, headings locales. Truncado a 200 entradas en docs gigantes. |
| `pages_truncated`             | `true` si `page_count > 200`; usa `sections[]` para navegar el resto.                                         |
| `estimated_tokens_full`       | Estimacion de tokens del Markdown completo (groser).                                                          |
| `estimated_tokens_spine_only` | Tokens del spine en si (para comparar con el coste de pedir secciones).                                       |
| `how_to_fetch_section`        | Recordatorio de como pedir el contenido real con `pages=`.                                                    |

## Ejemplo de flujo completo (agente IA)

```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. Indexar el PDF
r = httpx.post(f"{API}/pdf/index", json={"url": PDF}, headers=HEADERS)
spine = r.json()["spine"]
print(f"PDF tiene {spine['page_count']} paginas")
print(f"Tokens estimados completo: {spine['estimated_tokens_full']}")
print(f"Tokens del spine: {spine['estimated_tokens_spine_only']}")

# 2. Agente decide: solo quiere "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. Pedir SOLO ese rango
r = httpx.post(
    f"{API}/convert/from-url",
    json={"url": PDF, "pages": pages, "mode": "fast"},
    headers=HEADERS,
)
markdown_results = r.text
```

## Coste y rendimiento

* **Latencia tipica**: 300-700ms para PDFs hasta 512MB (sample de 32 paginas + cabeza de cada una).
* **Coste server**: constante respecto al tamano del PDF (no escala con paginas).
* **Coste cliente IA**: spine \~1.5K-4K tokens vs Markdown completo 50K-300K tokens.

| Caso                                                | PDF  |                   Sin indice |                        Con indice |
| --------------------------------------------------- | ---- | ---------------------------: | --------------------------------: |
| 500 paginas, agente quiere 1 seccion (\~12 paginas) | 10MB | 250K tokens, \~5s extraccion | **15K tokens, \~0.7s extraccion** |
| 1000 paginas, agente quiere 3 secciones             | 30MB |                  600K tokens |                    **20K tokens** |
| 50 paginas, agente quiere todo                      | 1MB  |                   25K tokens |      indice no aporta (no usarlo) |

## Limitaciones v1

* `pages[]` truncado a 200 entradas (configurable via `PDF_SPINE_MAX_PAGE_ENTRIES`). Para docs >200 paginas, usa `sections[]` como mapa de navegacion.
* Deteccion de tablas no incluida v1. Solo headings + body chars.
* Sin cache servidor: el cliente conserva el spine. Repetir `/pdf/index` re-descarga y re-procesa.

## Seguridad

* Mismo anti-SSRF que `/convert/from-url`: solo HTTPS, hosts publicos.
* Validacion de header de PDF y enforce de `MAX_PDF_PAGES`.
* API key + credito (`tier=index`, coste fijo 1 credito por llamada).
* No se guardan PDFs ni spine en el servidor.

Ver tambien: [POST /convert/from-url](/docs/public/es/api/convert-from-url), [Modos](/docs/public/es/concepts/modes), [Compresion](/docs/public/es/concepts/compression).
