# PDF Text Extractor – PDF to Text & Markdown (`sfoln/pdf-text-extractor`)

Extract the text of any PDF (URL, upload or key-value record) as Markdown and plain text, page by page, with metadata, outline and scanned-page detection. No OCR, no start fee: $3 per 1,000 PDFs up to 20 pages, $0.10 per 1,000 extra pages.

- Family: Documents & media
- Categories: AI, Developer tools, Automation
- Availability: on request (private on Apify). Email contact@sfoln.com with subject "Access request: PDF Text Extractor – PDF to Text & Markdown (sfoln/pdf-text-extractor)" before calling; calls fail until it is public.
- JSON: https://www.sfoln.com/actors/pdf-text-extractor.json
- MCP: https://mcp.apify.com?tools=sfoln/pdf-text-extractor
- REST (sync): POST https://api.apify.com/v2/actors/sfoln~pdf-text-extractor/run-sync-get-dataset-items
- REST (async): POST https://api.apify.com/v2/actors/sfoln~pdf-text-extractor/runs

PDF Text Extractor turns any PDF into clean Markdown and plain text, page by page, straight from the PDF's own text layer: headings, lists and simple tables are rebuilt from the layout, and you also get the document metadata (title, author, producer, dates), the bookmarks and a per-page verdict on which pages are scans without text. Give it PDF URLs, an uploaded file, data: URIs or Apify key-value store records; each PDF becomes one dataset record. Built for RAG pipelines, search indexing, LLM prompts and AI agents that need the text of born-digital PDFs fast and cheap.

## Price per delivered record

| Event | Price | Per 1,000 | Charged for |
| --- | --- | --- | --- |
| PDF document (`pdf`) | $0.003 | $3 / 1,000 | One PDF read from its text layer: text, Markdown, metadata and per-page details for up to 20 pages. |
| Extra PDF page (`pdf-page`) | $0.0001 | $0.10 / 1,000 | Each page returned beyond the first 20 of a PDF. |

Pay-per-event, USD, billed by Apify; no platform usage charge. Only records with status `ok` are charged. Cap a run with `maxTotalChargeUsd`. Apify free plan: up to 10 per run.

## Run

```bash
curl -X POST "https://api.apify.com/v2/actors/sfoln~pdf-text-extractor/run-sync-get-dataset-items" \
  -H "Authorization: Bearer $APIFY_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"pdfUrls":["https://www.w3.org/WAI/ER/tests/xhtml/testfiles/resources/pdf/dummy.pdf"]}'
```

MCP client configuration:

```json
{
  "mcpServers": {
    "apify": {
      "type": "http",
      "url": "https://mcp.apify.com?tools=sfoln/pdf-text-extractor"
    }
  }
}
```


## Input

The PDFs to read: URLs, an uploaded file, data: URIs or key-value store records. Each PDF produces one dataset record with its text, Markdown, metadata and per-page details.

| Field | Type | Default | Meaning |
| --- | --- | --- | --- |
| `pdfUrls` | array | — | PDF URLs: PDFs to extract, one per line: public http(s) URLs, data: URIs (base64) or Apify key-value store records (kvs://<storeId>/<key>, or the record's API URL, read with the run's token). Max 50 MB per PDF; the file type is detected from its bytes, not its name. Accounts on the Apify free plan can process up to 10 PDFs per run (the first 20 pages of each). |
| `pdfFile` | string | — | Or upload a PDF: A PDF from your computer (or its URL). It is processed together with the PDF URLs above. |
| `pages` | string | — | Pages: Page range to extract, e.g. "1-5, 8, 10-" ("10-" = from page 10 to the end, "-3" = the first three pages). Empty = every page. |
| `outputFormat` | string | `"both"` | Output format: Markdown (headings, lists and tables rebuilt from the layout), plain text, or both. One of `"both"`, `"markdown"`, `"text"`. |
| `includePages` | boolean | `true` | Per-page output: Return a pages[] list with each page's text, Markdown and text-layer verdict (besides the whole document's text). |
| `includeOutline` | boolean | `false` | Include bookmarks (outline): Return the PDF's bookmarks (table of contents) with the page each one points to. |
| `password` | string | — | PDF password: Password of encrypted PDFs (used for every PDF in this run). It is never written to the output. |
| `maxPages` | integer | `1000` | Max pages per PDF: Read at most this many pages of each PDF (after the page range). Pages beyond 20 are charged per page. |
| `maxItems` | integer | `1000` | Max PDFs: Safety cap on how many PDFs are processed in this run; further ones are ignored and counted as inputItemsDropped in the OUTPUT record. |
| `concurrency` | integer | `2` | Concurrency: PDFs processed in parallel (1–5). |
| `includeRaw` | boolean | `false` | Include raw metadata: Attach the PDF's raw Info dictionary and XMP properties to each record (raw). |

Example input:

```json
{
  "pdfUrls": [
    "https://www.w3.org/WAI/ER/tests/xhtml/testfiles/resources/pdf/dummy.pdf"
  ]
}
```

## Output

One record per input item with `status` (`ok` is charged; `not_found`, `error`, `skipped_*` are free) and `charged`. The run stores an `OUTPUT` summary record.

| Field | Type | Meaning |
| --- | --- | --- |
| `input` | string | object | Input: The PDF reference as submitted (data: URIs are shown as a short label). |
| `status` | string | Status: ok \| not_found \| error \| skipped_invalid \| skipped_budget \| skipped_free_plan \| skipped_unmetered |
| `charged` | boolean | Charged: Whether the record's pdf event was charged. |
| `chargedEvent` | string | Charged event: Pay-per-event event charged for this record (present only when charged). |
| `chargedCount` | integer | Events charged: Events actually charged when the charge came back short (present only then). |
| `source` | string | Source: pdf-text-layer: the PDF's own text layer, read in the Actor. |
| `fetchedAt` | string | Fetched at: ISO timestamp. |
| `error` | object | Error: { code, message, status? } when status is not ok. Codes: invalid_reference, document_too_large, document_not_found, not_a_pdf, not_readable, encrypted_pdf, wrong_password, corrupt_pdf, no_pages, timeout, fetch errors (blocked_url, dns_not_found, network, http_error, …) and the runtime codes (budget_exhausted, free_plan_limit, …). |
| `warnings` | array | Warnings: Notes: page limits, budget limits, text moved to the key-value store. |
| `url` | string | null | URL: URL the PDF was downloaded from (after redirects); the record URL for key-value store records; null for data: URIs. |
| `fileName` | string | null | File name |
| `fileSizeBytes` | integer | null | File size (bytes) |
| `sha256` | string | null | SHA-256: Hash of the PDF bytes (deduplication). |
| `title` | string | null | Title: Document title from the metadata, else the first heading. |
| `pageCount` | integer | null | Pages in PDF |
| `pagesExtracted` | integer | null | Pages extracted |
| `pageRange` | string | null | Page range: The requested page range, or null for every page. |
| `possiblyTruncated` | boolean | null | Truncated: Fewer pages were returned than the range selects (maxPagesPerPdf, free plan, budget or time limit). |
| `metadata` | object | null | Metadata: { title, author, subject, keywords[], creator, producer, createdAt, modifiedAt, pdfVersion, language, pageCount, encrypted, linearized, hasForm, pageWidth, pageHeight } |
| `language` | string | null | Language: Detected language of the text (ISO 639-1). |
| `hasTextLayer` | boolean | null | Has text layer: At least one returned page has extractable text. |
| `textPages` | integer | null | Pages with text: Pages with a usable text layer. |
| `scannedPages` | array | null | Scanned pages: Pages without a usable text layer that show an image or drawings: OCR them with pdf-ocr-markdown. |
| `blankPages` | array | null | Blank pages |
| `ocrRecommended` | boolean | null | OCR recommended |
| `ocrSuggestion` | string | null | OCR suggestion |
| `charCount` | integer | null | Characters |
| `wordCount` | integer | null | Words |
| `markdown` | string | null | Markdown: All returned pages, separated by <!-- page N --> comments. |
| `markdownUrl` | string | null | Full Markdown URL: Set when the text exceeded the dataset item size: the full Markdown is in the run's key-value store; markdown holds the first 1 MB. |
| `text` | string | null | Plain text: All returned pages as plain text, pages separated by a form feed (\f). |
| `textUrl` | string | null | Full text URL |
| `extraPagesCharged` | integer | null | Extra pages charged: pdf-page events charged (pages beyond 20). |
| `pages` | array | null | Pages: [{ pageNumber, markdown, text, charCount, wordCount, hasTextLayer, textQuality (good \| sparse \| garbled \| none), needsOcr, blank, imageCount, width, height }] |
| `outline` | array | null | Outline: Bookmarks [{ title, pageNumber, level }] (includeOutline). |
| `raw` | object | null | Raw metadata: { info, xmp } (includeRaw). |

Example record:

```json
{
  "input": "https://example.com/reports/q1-2026.pdf",
  "status": "ok",
  "charged": true,
  "chargedEvent": "pdf",
  "source": "pdf-text-layer",
  "fetchedAt": "2026-10-14T10:00:00.000Z",
  "url": "https://example.com/reports/q1-2026.pdf",
  "fileName": "q1-2026.pdf",
  "fileSizeBytes": 3337,
  "sha256": "5d1a0b2f8c3e4d6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b1c2d3e4f5a6b7c8d9e0f",
  "title": "Quarterly Report 2026",
  "pageCount": 3,
  "pagesExtracted": 3,
  "pageRange": null,
  "possiblyTruncated": false,
  "metadata": {
    "title": "Quarterly Report 2026",
    "author": "Example Analyst",
    "subject": "Q1 results",
    "keywords": [
      "report",
      "revenue"
    ],
    "creator": "Writer",
    "producer": "LibreOffice 24.2",
    "createdAt": "2026-04-02T09:30:00.000Z",
    "modifiedAt": "2026-04-02T09:30:00.000Z",
    "pdfVersion": "1.7",
    "language": "en-US",
    "pageCount": 3,
    "encrypted": false,
    "linearized": false,
    "hasForm": false,
    "pageWidth": 595.3,
    "pageHeight": 841.9
  },
  "language": "en",
  "hasTextLayer": true,
  "textPages": 3,
  "scannedPages": [],
  "blankPages": [],
  "ocrRecommended": false,
  "ocrSuggestion": null,
  "charCount": 717,
  "wordCount": 118,
  "markdown": "# Quarterly Report 2026\n\n## 1. Summary\n\nRevenue grew in every region during the first quarter of 2026.\n\n- Revenue grew 12 percent year over year.\n- Gross margin improved to 61 percent.\n\n<!-- page 2 -->\n\n## 2. Results by region\n\n| Region | Q1 revenue | Change |\n| --- | --- | --- |\n| North | 1,240 | +14% |\n| South | 980 | +9% |",
  "markdownUrl": null,
  "text": "Quarterly Report 2026\n\n1. Summary\n\nRevenue grew in every region during the first quarter of 2026.\n\n\f\n\n2. Results by region\n\nRegion\tQ1 revenue\tChange",
  "textUrl": null,
  "extraPagesCharged": 0,
  "pages": [
    {
      "pageNumber": 1,
      "markdown": "# Quarterly Report 2026\n\n## 1. Summary\n\n…",
      "text": "Quarterly Report 2026\n\n1. Summary\n\n…",
      "charCount": 303,
      "wordCount": 50,
      "hasTextLayer": true,
      "textQuality": "good",
      "needsOcr": false,
      "blank": false,
      "imageCount": 0,
      "width": 595.3,
      "height": 841.9
    }
  ]
}
```

## FAQ

**Does it read scanned PDFs?**

It reads the PDF's own text layer, which scans do not have. Pages without a usable text layer are listed in scannedPages and ocrSuggestion; run PDF OCR to Markdown (pdf-ocr-markdown) on such PDFs, which OCRs only those pages.

**How are pages counted and charged?**

Each PDF costs one pdf event, which covers up to 20 pages. Every further page costs one pdf-page event ($0.0001). Use the pages range or maxPages to read only the part you need.

**Can it open password-protected PDFs?**

Yes, when you set the password input. Without it (or with a wrong one) the record has status error with code encrypted_pdf or wrong_password and is not charged.


## Related

- [PDF OCR to Markdown Converter](https://www.sfoln.com/actors/pdf-ocr-markdown.md) (`sfoln/pdf-ocr-markdown`)
- [Document to Markdown – DOCX, PPTX, XLSX](https://www.sfoln.com/actors/document-to-markdown.md) (`sfoln/document-to-markdown`)
- [Document Parser – AI PDF & Image to JSON](https://www.sfoln.com/actors/document-to-json.md) (`sfoln/document-to-json`)
- [URL to Markdown & Article Extractor](https://www.sfoln.com/actors/url-to-markdown-summary.md) (`sfoln/url-to-markdown-summary`)

Publisher: SFOLN LLC, contact@sfoln.com. Catalogue: https://www.sfoln.com/catalog.json. Agent guide: https://www.sfoln.com/agents.md.
