---
title: "Ingest: inspect a PDF, no job, no charge"
method: POST
path: "/v1/render/from-pdf/analyze"
tags: ["pdf"]
---

# Ingest: inspect a PDF, no job, no charge

`POST /v1/render/from-pdf/analyze`

## Request

- Base URL: `https://api.slideforge.dev`
- URL: `https://api.slideforge.dev/v1/render/from-pdf/analyze`
- Auth: one of:
  - HTTP bearer
  - API key in header `X-API-Key`

Free preview endpoint — no charge, no persistence.

Logs each call's producer + page count + source classification to
slideforge.telemetry so we can see what real users upload and prioritize
which producers to support next. Fire-and-forget — never fails the
request.

## Response `200`

Successful Response

- PdfAnalysis: Output of analyze_pdf() — what /analyze returns and what /generate uses to route.
  - `producer` string: PDF metadata producer string (telemetry only — never used for routing). Captured so we can correlate buckets with real-world tools after the fact.
  - `page_count` integer, required: Number of pages in the PDF
  - `file_size_bytes` integer, required: Source PDF size in bytes
  - `bucket` 'slide-vector' | 'slide-raster' | 'doc-text' | 'doc-image-heavy' | 'scanned' | 'mixed' | 'unknown', required: Routing decision derived from structural features. The /v1/deck/from-pdf endpoint accepts a file iff its bucket is currently implemented.
  - `features` PdfFeatures, required: Document-level structural features used by the bucket classifier. Everything here comes from `fitz` primitives — no LLM calls. The fields are deliberately concrete (counts, ratios, booleans) so threshold tuning against a real corpus is reproducible.
    - `aspect_ratio_first` number, required: width / height of the first page
    - `is_slide_aspect` boolean, required: True if the first page sits in a slide-shaped aspect band (roughly 1.25–2.45 — covers 4:3, 16:10, 16:9, ultrawide)
    - `is_doc_portrait` boolean, required: True if the first page is portrait-oriented (aspect < 1.0)
    - `consistent_dimensions` boolean, required: True if every page in the document shares the same width/height
    - `total_text_chars` integer, required: Sum of extractable characters across all pages
    - `total_text_spans` integer, required: Sum of text spans (formatted runs) across all pages. Zero with non-zero total_text_chars suggests glyph-outlined text — readable but unformatted.
    - `total_images` integer, required: Sum of embedded images across all pages
    - `total_drawings` integer, required: Sum of vector drawing primitives (rects, paths, lines) across all pages — high counts indicate hand-built slide content rather than scanned/raster
    - `unique_fonts` integer, required: Number of distinct fonts seen across the doc
    - `pages_with_text` integer, required: Pages with at least 50 chars of extractable text
    - `pages_image_only` integer, required: Pages with images present but fewer than 50 chars of extractable text
    - `pages_blank` integer, required: Pages with no text and no images
  - `estimated_cost_usd` number, required: Per-page price for the bucket × page_count
  - `can_extract` boolean, required: True iff /v1/deck/from-pdf will currently accept this file.
  - `unsupported_reason` string, nullable: Human-readable hint shown to the user when can_extract is False, explaining what's not yet supported.
  - `pages` PdfPageInfo[]: Per-page summary, in document order
    - `page_number` integer, required: 1-based page number
    - `has_text` boolean, required: True if the page contains extractable text
    - `has_images` boolean, required: True if the page contains embedded images
    - `text_char_count` integer, required: Total characters of extractable text
    - `text_preview` string: First ~80 characters of text content (for the analysis card)

## Other responses

- `422`: Validation Error

---

[API](https://skmtc.dev/slideforge/apis/slideforge-api.md) · [All operations](https://skmtc.dev/slideforge/apis/slideforge-api/llms.txt) · [OpenAPI document](https://skmtc.dev/slideforge/apis/slideforge-api/revisions/ae698e3d3c00?raw)
