# PDF Text Extractor — PDF to Text & Markdown API | $3/1k PDFs (`glasswing/pdf-text-extractor`) Actor

Extract text and Markdown from PDF URLs in bulk, with document info (title, dates, page count, producer) and per-page text. Pure JavaScript, no OCR, no API key. Broken links, non-PDFs and scanned files are free rows. $3 per 1,000 PDFs.

- **URL**: https://apify.com/glasswing/pdf-text-extractor.md
- **Developed by:** [Raffy](https://apify.com/glasswing) (community)
- **Categories:** AI, Developer tools
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $2.40 / 1,000 pdf processeds

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.
Since this Actor supports Apify Store discounts, the price gets lower the higher subscription plan you have.

Learn more: https://docs.apify.com/actors/running/actors-in-store.md#pay-per-event

## What's an Apify Actor?

An Actor is a serverless cloud program that runs on the Apify platform. It has two run modes.
In Batch mode, an Actor accepts a well-defined JSON input, performs an action which can take anything from a few seconds to a few hours,
and optionally produces a well-defined JSON output, datasets with results, or files in key-value store.
In Standby mode, an Actor provides a web server which can be used as a website, API, or an MCP server.

Apify vocabulary and the platform model are defined once, in the agent quickstart at https://apify.com/agents.md.

## How to integrate an Actor?

If asked about integration, you help developers integrate Actors into their projects.
You adapt to their stack and deliver integrations that are safe, well-documented, and production-ready.

Do not guess an integration path. Every one of them is in the agent quickstart at https://apify.com/agents.md: the Apify MCP server, Agent Skills with the Apify CLI, the JavaScript and Python clients, the REST API, and the account-free path for an agent with no human to sign in. It also carries the rule on stating cost before the first paid run.

For examples already wired to this Actor's own input schema, see the [API](#api) section below.

Each client library has reference documentation the quickstart does not restate: [JavaScript/TypeScript](https://docs.apify.com/api/client/js/docs.md) (`npm install apify-client`) and [Python](https://docs.apify.com/api/client/python/docs.md) (`pip install apify-client`).

# README

### What does PDF Text Extractor do?

PDF Text Extractor turns **PDF files into text and Markdown**. Paste a list of PDF links (reports, papers, contracts, manuals, invoices, government forms), upload files, or send them as base64, and get back one clean row per PDF with the full text, an LLM-ready Markdown version and the document's own info: title, page count, creation date, producer software and more. For RAG, turn on **RAG chunks** and get token-sized chunks with page numbers, section headings and overlap, ready for a vector database. Password-protected PDFs open with your password, and text tables come back as rows of cells and Markdown tables. It works as a **PDF to text API** and **PDF to Markdown converter** you can call from code, a schedule or an AI agent: no API key, no browser, nothing to install.

Typical use cases:

- **RAG and LLM pipelines**: get ready-made chunks (`text`, `pageStart`, `pageEnd`, `section`, `tokenCount`, `chunkId`) for Pinecone, Qdrant, Weaviate, pgvector, LangChain or LlamaIndex, or feed whole-document Markdown into your own chunker.
- **Document search and archiving**: index the text of hundreds of PDFs from your website, data room or document portal.
- **Research and analysis**: pull the text of papers, filings, standards and policy documents for summarising or classification.
- **Data entry and compliance**: read the text of forms and contracts, and check document info (dates, producer, encryption, form fields) in bulk.
- **AI agents**: a cheap PDF reader tool with plain inputs and self-describing rows.

### Why use this PDF text extractor?

- **Bulk by default.** Hundreds of PDF URLs in one run, downloaded in parallel; every PDF is one row.
- **Text and Markdown in one pass.** Plain text keeps the PDF's lines; Markdown rebuilds headings from font sizes, bullet and numbered lists and wrapped paragraphs, and removes page numbers and the running headers and footers repeated across pages.
- **RAG chunks with citations.** One row per chunk of about `chunkSizeTokens` tokens (default 500), split at paragraphs and sentences, with a configurable overlap, the page numbers each chunk comes from and the heading it sits under. A PDF of up to 50 chunks costs the same as in the default mode.
- **Files, not only links.** Upload PDFs in Apify Console, point at records in your key-value stores (read with your own run token, so private stores work), or send base64 from your code.
- **Password-protected PDFs.** Give the password once (stored encrypted, never logged or written out); a wrong password is a free `wrong_password` row.
- **Text tables.** With `extractTables`, tables in the text layer become a `tables` array (rows of cells) and Markdown tables, so numbers stay in their columns.
- **Per-page text when you need it.** Turn on `includePageText` for a `pages` array (still one result per PDF), or `splitByPage` for one row per page.
- **Honest results, honest billing.** Every row has a `status` (`ok`, `not_found`, `error`) and, when something went wrong, an `errorType`. A broken link, a web page instead of a PDF, a password-protected, damaged, oversized or scanned PDF is one free row, and the rest of the batch still runs. Only PDFs whose text was extracted are charged.
- **Simple, flat price.** $3 per 1,000 PDFs (up to 100 pages each by default), whatever the page count.
- **Share links just work.** Google Drive, Dropbox and GitHub file links and arXiv abstract pages are turned into their download links automatically.

### What data can PDF Text Extractor extract?

One row per PDF (default):

| Field | Type | Description |
|---|---|---|
| `fileName` | string | File name from the server's Content-Disposition header or the URL |
| `title` | string | Title from the PDF's document info (or XMP metadata) |
| `pageCount` | integer | Total pages in the PDF |
| `pagesExtracted` | integer | Pages read for this row (after `pageRange` and `maxPages`) |
| `extractedPages` | string | Which pages were read, e.g. `1-5, 8` |
| `pageNumber` | integer | Page number, only on rows written with `splitByPage` |
| `charCount` | integer | Characters of extracted text |
| `wordCount` | integer | Words of extracted text |
| `needsOcr` | boolean | `true` when the text layer is missing (scanned or image-only pages) |
| `pagesWithoutText` | integer | Pages read that had no text layer |
| `truncated` | boolean | `true` when `maxPages` or the timeout stopped the PDF early; `warning` says how far it got |
| `text` | string | Plain text, line by line as in the PDF, blank lines between blocks and pages |
| `markdown` | string | Markdown with `#` headings, `-` and `1.` lists and reflowed paragraphs |
| `subject`, `keywords` | string | The PDF's document info fields (the author field is never returned: it usually holds a person's name) |
| `creator`, `producer` | string | The application that made the document and the PDF |
| `creationDate`, `modificationDate` | string | ISO 8601 dates in UTC |
| `pdfVersion`, `language` | string | e.g. `1.7`, `en-US` |
| `hasForms`, `isEncrypted` | boolean | Fillable form fields present; permissions-only encryption (it still opens) |
| `fileSizeBytes`, `contentType` | integer, string | The downloaded file's size and the Content-Type the host sent |
| `pages` | array | With `includePageText`: `[{ pageNumber, text, markdown, charCount, hasText }]` |
| `tables`, `tableCount` | array, integer | With `extractTables`: `[{ pageNumber, rowCount, columnCount, rows: [["Region", "Q1"], ["North", "1,200"]] }]` |
| `source` | string | `base64` or `keyValueStore` for files that did not come from a URL |
| `inputUrl`, `finalUrl` | string | The link as you typed it (when rewritten) and where redirects ended |
| `url` | string | The PDF URL that was downloaded |
| `status` | string | `ok`, `not_found` or `error` (see below) |
| `errorType` | string | Machine-readable reason on rows that are not `ok` |
| `error` | string | Human-readable reason on rows that are not `ok` |
| `scrapedAt` | string | ISO 8601 time the PDF was processed |

RAG chunk rows (`ragChunks`), one per chunk:

| Field | Type | Description |
|---|---|---|
| `text` | string | The chunk: Markdown (headings, lists, tables) unless Output format is plain text (`chunkFormat` says which) |
| `chunkId` | string | Stable id: the same PDF URL gives the same ids on every run (`<hash>-<index>`) |
| `chunkIndex`, `chunkCount` | integer | Position of the chunk (from 0) and the number of chunks in the PDF |
| `pageStart`, `pageEnd`, `pageNumbers` | integer, integer, array | The page(s) the chunk's text comes from, for citations |
| `section` | string | The nearest heading above the chunk |
| `tokenCount` | integer | Estimated tokens (about 4 characters of English per token, 1 per CJK character) |
| `fileName`, `title`, `pageCount`, `url` | | The PDF the chunk belongs to (on every chunk); the other document info fields are on chunk 0 |

#### Result status (tri-state output)

| `status` | `errorType` | Meaning | Charged? |
|---|---|---|---|
| `ok` | - | Text was extracted | Yes, one result (RAG chunks: one per started 50 chunks of a PDF) |
| `not_found` | `http_404`, `http_410` | No file at the URL | No |
| `not_found` | `no_text` | The PDF (or page) has no text layer: scanned or image-only; `needsOcr` is `true` | No |
| `not_found` | `no_pages_selected` | `pageRange` selects no page of this PDF | No |
| `error` | `not_pdf` | The URL returned a web page or another file type, not a PDF | No |
| `error` | `too_large` | Over `maxFileSizeMb` (checked before downloading when the host sends the size) | No |
| `error` | `password_protected` | The PDF needs a password to open and none was given | No |
| `error` | `wrong_password` | The password given does not open the PDF | No |
| `error` | `invalid_input` | A `base64Files` / `fileUploads` entry could not be used | No |
| `error` | `malformed` | The file is damaged or not a valid PDF | No |
| `error` | `timeout` | The download or the reading ran out of time | No |
| `error` | `blocked`, `rate_limited`, `http_error`, `network`, `download_failed` | The host refused or failed the download (after retries) | No |
| `error` | `output_too_large` | The text is larger than one dataset row can hold (9 MB); use `splitByPage` | No |

The run itself still succeeds when some PDFs fail, so one bad link never costs you the rest of the batch.

### How to use the PDF to text converter

1. Click **Try for free** and paste your PDF links into **PDF URLs**, one per line, or upload files under **Upload PDFs or key-value store records**.
2. Pick the **Output format**: text and Markdown (default), text only, or Markdown only.
3. Optional: set a **Page range** such as `1-10`, turn on **Include per-page text**, **One row per page**, **RAG chunks** or **Extract tables**, or enter a **PDF password**.
4. Click **Start**. A typical PDF takes a second or two to read; the default example (two government PDFs, 34 pages) finishes in under 20 seconds.
5. Download the results as JSON, CSV, Excel or HTML, or read them through the Apify API.

From code, call the Actor with the [Apify API](https://docs.apify.com/api/v2) or the Apify client for JavaScript or Python and pass the same input as JSON.

### How much does it cost to extract text from PDFs?

Pay-per-event pricing; Apify platform usage (compute) is included, so you pay only these events:

| Event | Price |
|---|---|
| Actor start | $0.005 per run |
| PDF processed (`status: ok` row) | $0.003 per PDF ($3 per 1,000) |

Examples:

- 1 PDF: $0.005 + $0.003 = **$0.008**.
- 100 PDFs of any length (up to `maxPages`, 100 by default): $0.005 + 100 x $0.003 = **$0.305**.
- 1,000 PDFs: **$3.005**.
- With **One row per page**, every page with text is one result: a 20-page PDF costs 20 x $0.003 = $0.06. For per-page text at the per-PDF price, use **Include per-page text** instead.
- With **RAG chunks**, a PDF is one result per started 50 chunks: a 30-page report in 500-token chunks (about 40 chunks) is **$0.003**, a 100-page manual (about 140 chunks) is 3 x $0.003 = **$0.009**. That is $0.06 per 1,000 chunks at most.
- Uploaded files, key-value store records and base64 files cost the same as URLs.

Broken links, non-PDF files, password-protected, damaged or oversized files and scanned PDFs without a text layer are free rows. Cap spending with **Maximum rows** and the run's **Max total charge**.

### Input

| Field | Type | Default | Description |
|---|---|---|---|
| `startUrls` | array | two example PDFs | PDF links, one per line; share links are rewritten to download links |
| `fileUploads` | array | none | Uploaded files, key-value store record URLs, or `storeId/key`; read with your run's token |
| `base64Files` | array | none | `["JVBERi0..."]` or `[{ "fileName": "a.pdf", "base64": "JVBERi0...", "password": "..." }]`; a data URL works too |
| `password` | string (secret) | none | Opens password-protected PDFs; never logged or output |
| `outputFormat` | string | `both` | `both`, `text` or `markdown` |
| `splitByPage` | boolean | `false` | One row per page (each page with text is one result) |
| `includePageText` | boolean | `false` | Add a `pages` array with every page's text to each PDF row |
| `ragChunks` | boolean | `false` | One row per token-sized chunk (replaces `splitByPage`) |
| `chunkSizeTokens` | integer | `500` | Chunk size in estimated tokens, 100 to 2,000 |
| `chunkOverlapTokens` | integer | `50` | Tokens repeated from the previous chunk, 0 to 500 (at most half a chunk) |
| `extractTables` | boolean | `false` | Text tables as a `tables` array and Markdown tables |
| `pageRange` | string | all pages | e.g. `1-5, 8, 12-` (`12-` = to the end, `-3` = first three) |
| `maxPages` | integer | `100` | Pages read per PDF, up to 1,000 |
| `includeMetadata` | boolean | `true` | Add the PDF's document info fields |
| `maxFileSizeMb` | integer | `50` | Skip larger files, up to 100 MB |
| `timeoutSecs` | integer | `120` | Time allowed per PDF for downloading, and again for reading |
| `maxItems` | integer | `1000` | Safety cap on rows written (with RAG chunks: on PDFs) |
| `proxyConfiguration` | object | off | Apify Proxy, only for hosts that block direct downloads |

Example input (RAG chunks, with a private file from a key-value store and one sent as base64):

```json
{
    "startUrls": ["https://nvlpubs.nist.gov/nistpubs/CSWP/NIST.CSWP.29.pdf"],
    "fileUploads": ["<storeId>/annual-report.pdf"],
    "base64Files": [{ "fileName": "contract.pdf", "base64": "JVBERi0xLjcK...", "password": "your-password" }],
    "ragChunks": true,
    "chunkSizeTokens": 500,
    "chunkOverlapTokens": 50,
    "extractTables": true
}
```

Example input (one row per PDF):

```json
{
    "startUrls": [
        "https://nvlpubs.nist.gov/nistpubs/CSWP/NIST.CSWP.29.pdf",
        "https://www.irs.gov/pub/irs-pdf/p1.pdf"
    ],
    "outputFormat": "both",
    "pageRange": "1-20",
    "includePageText": true
}
```

### Output

Real rows from the default run (2026-10-16), with `text` and `markdown` shortened:

```json
[
    {
        "url": "https://www.irs.gov/pub/irs-pdf/p1.pdf",
        "status": "ok",
        "fileName": "p1.pdf",
        "title": "Publication 1 (Rev. 9-2017)",
        "pageCount": 2,
        "pagesExtracted": 2,
        "extractedPages": "1-2",
        "charCount": 11376,
        "wordCount": 1881,
        "needsOcr": false,
        "pagesWithoutText": 0,
        "truncated": false,
        "text": "Your Rights\nas a Taxpayer\n\nThe IRS Mission\n\nThe Taxpayer Bill of Rights\n\nPublication 1\n\nThis publication explains your rights as a taxpayer and the processes for examination, appeal, collection, and refunds.\nAlso available in Spanish. ...",
        "markdown": "# Your Rights as a Taxpayer\n\n## The IRS Mission\n\n# The Taxpayer Bill of Rights\n\n## Publication 1\n\nThis publication explains your rights as a taxpayer and the processes for examination, appeal, collection, and refunds. Also available in Spanish. ...\n\n## 1. The Right to Be Informed\n\nTaxpayers have the right to know what they need to do to comply with the tax laws. ...",
        "subject": "Your Rights as a Taxpayer",
        "creator": "Adobe Illustrator CS6 (Windows)",
        "producer": "Adobe PDF library 10.01",
        "creationDate": "2017-10-31T16:08:08.000Z",
        "modificationDate": "2017-10-31T16:16:24.000Z",
        "pdfVersion": "1.4",
        "hasForms": false,
        "isEncrypted": false,
        "fileSizeBytes": 324302,
        "contentType": "application/pdf",
        "scrapedAt": "2026-10-15T23:47:42.646Z"
    },
    {
        "url": "https://nvlpubs.nist.gov/nistpubs/CSWP/NIST.CSWP.29.pdf",
        "status": "ok",
        "fileName": "NIST.CSWP.29.pdf",
        "title": "The NIST Cybersecurity Framework (CSF) 2.0",
        "pageCount": 32,
        "pagesExtracted": 32,
        "extractedPages": "1-32",
        "charCount": 68724,
        "wordCount": 9421,
        "needsOcr": false,
        "text": "National Institute of Standards and Technology\n\nThis publication is available free of charge from: https://doi.org/10.6028/NIST.CSWP.29\n\nFebruary 26, 2024\n\nThe NIST Cybersecurity\nFramework (CSF) 2.0 ...",
        "markdown": "### National Institute of Standards and Technology\n\nThis publication is available free of charge from: https://doi.org/10.6028/NIST.CSWP.29\n\nFebruary 26, 2024\n\n# The NIST Cybersecurity Framework (CSF) 2.0 ...",
        "keywords": "cybersecurity; Cybersecurity Framework (CSF); cybersecurity risk governance; cybersecurity risk management; enterprise risk management; Profiles; Tiers",
        "creator": "Acrobat PDFMaker 23 for Word",
        "producer": "Adobe PDF Library 23.8.53",
        "creationDate": "2024-03-06T20:46:45.000Z",
        "pdfVersion": "1.6",
        "language": "EN-US",
        "fileSizeBytes": 1518858,
        "scrapedAt": "2026-10-15T23:47:43.244Z"
    }
]
```

A link that is not a PDF comes back like this, free:

```json
{
    "url": "https://example.org/annual-report",
    "status": "error",
    "errorType": "not_pdf",
    "error": "The URL returned a web page (text/html), not a PDF file. Use the direct link to the .pdf file; many sites show a viewer or download page first. Not charged.",
    "contentType": "text/html"
}
```

### Tips

- Use **Markdown** for LLMs and RAG: headings and lists survive, and page numbers and the running headers and footers repeated at the top or bottom of the pages are removed.
- Long PDFs: raise **Max pages per PDF** (up to 1,000) or read the part you need with **Page range**.
- Need page numbers for citations? **Include per-page text** keeps one result per PDF and adds every page in `pages`.
- Filter failures by `errorType` instead of parsing `error`; `needsOcr: true` marks scanned documents to send to an OCR tool.

### Limitations

- **No OCR.** Scanned or image-only PDFs have no text layer; they come back as free `not_found` rows with `needsOcr: true`. PDFs that already carry an OCR text layer are read normally.
- **Tables: text tables only.** `extractTables` finds tables whose cells sit in columns separated by white space in the text layer, row by row. Tables that are images, cells drawn column by column, merged cells and very irregular layouts stay as plain text lines; a two-column page whose columns share baselines may come out as a two-column table. Check the `tables` you rely on.
- **Token counts are estimates.** `tokenCount` uses about 4 characters per token for English and 1 per CJK character, not a model tokenizer; keep a margin under your embedding model's limit.
- **Running headers and footers** are recognised when the same text (up to three lines) repeats at the top or bottom of at least 3 pages and 40% of the pages read; on shorter documents only page numbers are removed. The plain `text` field always keeps every line.
- **Headings come from font size.** Bold text at body size is not detected as a heading, and images, charts and equations are not converted.
- **Reading order follows the PDF.** Most PDFs (Word, LaTeX, InDesign exports) come out column by column; a PDF whose internal order is scrambled comes out scrambled.
- **Hyphenation.** For TeX-made PDFs a hyphen at a line end is removed when the word continues on the next line; for other producers it is kept (`high-level`), which occasionally leaves a split word with a hyphen.
- **Passwords.** PDFs that need a password to open are read when you give the password (`password`, or per file in `base64Files`); without it they are free `password_protected` rows. Permissions-only encryption is fine. The Actor never guesses or cracks passwords.
- **Base64 size.** Base64 files travel inside the run input, which Apify limits in size; send large files as key-value store records instead.
- **Size and length.** Up to 100 MB and 1,000 pages per PDF; one dataset row holds at most about 9 MB of text (use `splitByPage` for huge documents).
- **The host decides.** A file behind a login, a cookie wall or an anti-bot challenge cannot be downloaded; you get a free `blocked` row. Try Apify Proxy for hosts that block direct downloads.

### FAQ

#### Is there a PDF to text API I can call from my code?

Yes. Every Apify Actor is an API: start a run with your PDF links and read the dataset as JSON, CSV or Excel, or call it synchronously and get the rows in the response. The input and output are the tables above.

#### Can it convert PDF to Markdown for ChatGPT, Claude or a RAG pipeline?

Yes, that is what the `markdown` field is for: headings, lists and paragraphs are rebuilt and page furniture (page numbers, and running headers and footers repeated across pages) is removed, so the text chunks cleanly. Or let the Actor chunk it: **RAG chunks** writes one row per chunk with the page numbers, the section heading, an estimated token count and a stable `chunkId`, ready to embed.

#### Can I process PDFs that are not online?

Yes. Upload them in Apify Console (**Upload PDFs or key-value store records**), store them in a key-value store and pass `storeId/key`, or send them as base64 in `base64Files` from your code. Each file is billed like a URL.

#### Does it read scanned PDFs?

No. It reads the text layer that digital PDFs carry. A scanned PDF without one is reported as `not_found` with `needsOcr: true` and is not charged, so you can route it to an OCR tool.

#### Why is a PDF that opens in my browser reported as `not_pdf` or `blocked`?

Some sites link to a viewer or download page rather than the file, or only serve the file after a login or a challenge. Use the direct link to the `.pdf` file. For Google Drive, Dropbox and GitHub, paste the normal share link; it is converted for you.

#### Can I use it from an AI agent or MCP client?

Yes. The input has safe defaults, every row explains itself with `status` and `errorType`, and failed documents are free, so an agent can pass a list of links and read the result without special handling.

#### Is my document data stored or shared?

The Actor only downloads the URLs you give it, reads the key-value store records you point it at (with your own run's token) and the base64 files in your input, and writes the results to your own run's dataset in your Apify account. It does not send the documents anywhere else. A PDF password is a secret input: Apify stores it encrypted, and the Actor never logs it or writes it to the dataset.

### Legal and data-protection notice

This Actor processes only the documents you provide (links, uploaded files, key-value store records or base64) and returns what those documents contain, plus the technical document info stored in the PDF (title, dates, producer software). The PDF's author field is deliberately not returned. It does not log in, does not guess or crack passwords (it uses only the password you give it for your own documents), does not get around access controls and does not collect data from any website on its own. Documents can contain personal data. Personal data is protected by the GDPR in the European Union and by other regulations around the world. You should not process personal data unless you have a legitimate reason to do so, and you are responsible for having the right to process the documents you submit and for complying with applicable law.

This Actor is an independent tool. PDF is an open standard (ISO 32000); text extraction uses the open-source pdf.js library. All trademarks belong to their respective owners.

# Changelog

This Actor's version history is a separate document: https://apify.com/glasswing/pdf-text-extractor/changelog.md

# Actor input Schema

## `startUrls` (type: `array`):

Direct links to PDF files, one per line. Google Drive, Dropbox and GitHub share links and arXiv abstract pages are turned into their download links automatically. A link that does not return a PDF gives a free row with the reason.

## `fileUploads` (type: `array`):

Upload PDF files from your computer, or give records already in an Apify key-value store: the record's API URL or `storeId/key` (`username~store-name/key`). Records are read with your run's own token, so private stores work. Each file is one row (or chunk rows), billed like a URL.

## `base64Files` (type: `array`):

For API calls and automations: PDFs sent inside the input, each a base64 string (a `data:application/pdf;base64,...` URL works too) or an object `{ "fileName": "invoice.pdf", "base64": "JVBERi0...", "password": "optional" }`. The whole input must stay under Apify's input size limit; for bigger files use a key-value store record above.

## `password` (type: `string`):

Opens password-protected PDFs: tried on every file that needs a password (a password in a base64Files entry wins for that file). Stored encrypted, never logged or written to the dataset. A wrong password gives a free `wrong_password` row.

## `outputFormat` (type: `string`):

Plain text keeps the PDF's lines; Markdown rebuilds headings, lists and paragraphs (and tables with 'Extract tables'), good for LLMs and RAG. Both is the default.

## `ragChunks` (type: `boolean`):

For vector databases and RAG: write one row per token-sized chunk with `text`, page numbers (`pageStart`, `pageEnd`, `pageNumbers`), the section heading above it, an estimated `tokenCount`, a stable `chunkId` and overlap with the previous chunk. Chunks are Markdown unless Output format is plain text. Billing: one result per PDF per started 50 chunks (up to 50 chunks costs the same as one PDF).

## `chunkSizeTokens` (type: `integer`):

Target chunk size in estimated tokens (about 4 characters of English text per token; 1 per CJK character). Chunks break at paragraphs, then sentences, then words, and never exceed this size.

## `chunkOverlapTokens` (type: `integer`):

Tokens repeated from the end of one chunk at the start of the next, so a sentence cut at a boundary is still found. At most half the chunk size.

## `extractTables` (type: `boolean`):

Find tables in the PDF's text layer (columns separated by white space; no OCR) and return them as rows of cells in a `tables` array, and as Markdown tables in `markdown` and in RAG chunks. Tables that are images, or drawn column by column, stay as plain text lines.

## `splitByPage` (type: `boolean`):

Write one row per page instead of one row per PDF. Each page row with text is one billed result, so a 20-page PDF costs 20 results. For page-level text at the per-PDF price, leave this off and turn on 'Include per-page text' instead.

## `includePageText` (type: `boolean`):

Add a `pages` array to each PDF row with the text (and Markdown) of every page, so you can cite page numbers. Still one result per PDF.

## `pageRange` (type: `string`):

Read only these pages of every PDF, e.g. `1-5, 8, 12-` (`12-` = page 12 to the end, `-3` = the first 3 pages). Leave empty for all pages (up to Max pages).

## `maxPages` (type: `integer`):

Stop after this many pages of each PDF; the row then says `truncated: true` and how far it got. Up to 1,000.

## `includeMetadata` (type: `boolean`):

Add the PDF's own document info: title, subject, keywords, creator and producer software, creation and modification dates, PDF version, language, whether it has form fields or is encrypted. The author field is never returned (it usually holds a person's name).

## `maxFileSizeMb` (type: `integer`):

Larger PDFs are skipped with a free `error` row (checked from the Content-Length header before downloading, and again after). Up to 100 MB.

## `timeoutSecs` (type: `integer`):

Maximum time for downloading one PDF, and again for reading it. A PDF that runs out of time while being read returns the pages read so far with `truncated: true`.

## `maxItems` (type: `integer`):

Safety cap on rows written: PDFs, or pages with 'One row per page'. Rows with status `ok` are the billable results; `not_found` and `error` rows are free.

## `proxyConfiguration` (type: `object`):

Optional. Downloads go direct, which works for nearly every file host. Enable Apify Proxy (datacenter) only if a host blocks direct downloads.

## Actor input object example

```json
{
  "startUrls": [
    "https://nvlpubs.nist.gov/nistpubs/CSWP/NIST.CSWP.29.pdf",
    "https://www.irs.gov/pub/irs-pdf/p1.pdf"
  ],
  "outputFormat": "both",
  "ragChunks": false,
  "chunkSizeTokens": 500,
  "chunkOverlapTokens": 50,
  "extractTables": false,
  "splitByPage": false,
  "includePageText": false,
  "maxPages": 100,
  "includeMetadata": true,
  "maxFileSizeMb": 50,
  "timeoutSecs": 120,
  "maxItems": 1000,
  "proxyConfiguration": {
    "useApifyProxy": false
  }
}
```

# Actor output Schema

## `results` (type: `string`):

No description

# API

You can run this Actor programmatically using our API. Below are code examples in JavaScript, Python, and CLI, as well as the OpenAPI specification and MCP server setup.

## JavaScript example

```javascript
import { ApifyClient } from 'apify-client';

// Initialize the ApifyClient with your Apify API token
// Replace the '<YOUR_API_TOKEN>' with your token
const client = new ApifyClient({
    token: '<YOUR_API_TOKEN>',
});

// Prepare Actor input
const input = {
    "startUrls": [
        "https://nvlpubs.nist.gov/nistpubs/CSWP/NIST.CSWP.29.pdf",
        "https://www.irs.gov/pub/irs-pdf/p1.pdf"
    ],
    "proxyConfiguration": {
        "useApifyProxy": false
    }
};

// Run the Actor and wait for it to finish
const run = await client.actor("glasswing/pdf-text-extractor").call(input);

// Fetch and print Actor results from the run's dataset (if any)
console.log('Results from dataset');
console.log(`💾 Check your data here: https://console.apify.com/storage/datasets/${run.defaultDatasetId}`);
const { items } = await client.dataset(run.defaultDatasetId).listItems();
items.forEach((item) => {
    console.dir(item);
});

// 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/js/docs

```

## Python example

```python
from apify_client import ApifyClient

# Initialize the ApifyClient with your Apify API token
# Replace '<YOUR_API_TOKEN>' with your token.
client = ApifyClient("<YOUR_API_TOKEN>")

# Prepare the Actor input
run_input = {
    "startUrls": [
        "https://nvlpubs.nist.gov/nistpubs/CSWP/NIST.CSWP.29.pdf",
        "https://www.irs.gov/pub/irs-pdf/p1.pdf",
    ],
    "proxyConfiguration": { "useApifyProxy": False },
}

# Run the Actor and wait for it to finish
run = client.actor("glasswing/pdf-text-extractor").call(run_input=run_input)

# Fetch and print Actor results from the run's dataset (if there are any)
print(f"💾 Check your data here: https://console.apify.com/storage/datasets/{run.default_dataset_id}")
for item in client.dataset(run.default_dataset_id).iterate_items():
    print(item)

# 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/python/docs/quick-start

```

## CLI example

```bash
echo '{
  "startUrls": [
    "https://nvlpubs.nist.gov/nistpubs/CSWP/NIST.CSWP.29.pdf",
    "https://www.irs.gov/pub/irs-pdf/p1.pdf"
  ],
  "proxyConfiguration": {
    "useApifyProxy": false
  }
}' |
apify call glasswing/pdf-text-extractor --silent --output-dataset

```

## MCP server setup

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

The hosted server signs you in with OAuth on first connect, so no API token belongs in this config. Clients without OAuth support can send an `Authorization: Bearer <APIFY_API_TOKEN>` header instead, using a token from API & Integrations in Apify Console (https://console.apify.com/settings/integrations).

## OpenAPI specification

Download the OpenAPI definition: https://api.apify.com/v2/actors/V5pNjpCPDyEAhttMx/builds/2PUWyoPxwQ3D0Xmch/openapi.json
