# PDF to Text & Markdown Extractor (`far003/pdf-to-text-markdown-extractor`) Actor

Give it PDF URLs, get clean text, Markdown and metadata for each one, ready for LLM and RAG pipelines. Reading order, columns, headings, lists, links and bookmarks are rebuilt. No OCR. You pay only for PDFs whose text was extracted.

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

## Pricing

$2.50 / 1,000 pdf documents

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.

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

## PDF to Text & Markdown Extractor

Give it a list of PDF URLs. For every PDF you get clean **text**, **Markdown** and **metadata** in one dataset record, ready to feed an LLM, a vector store or a search index.

The text is not a raw dump of what the file contains. Lines are rebuilt from the position of every word, columns are read one after the other, words broken by a hyphen at the end of a line are joined, repeated page headers, footers and page numbers are dropped, and the Markdown carries headings, lists, links and code blocks.

**No OCR.** A scanned PDF (pages that are only pictures) has no text to extract. It is reported as `no_text` and costs nothing.

### Who it is for

- Teams that load PDFs (reports, filings, papers, manuals, specifications) into RAG pipelines and want text that reads in the right order.
- People who monitor a set of PDF documents and want to be told only about the ones that changed.
- Anyone who needs the same PDF facts in a stable shape: page count, outline (bookmarks), links, dates, size, SHA-256.

### What you pay for

You pay only for PDFs whose text was extracted (status `ok` or `partial`). Everything else is free: unreachable, not a PDF, blocked by robots.txt or by a bot wall, password-protected, scan without a text layer, a PDF whose text is not text (see `unreadable_text`), too large, unchanged since the last run, invalid address. A run also stops by itself when the spending limit you set is reached, and it does not download further PDFs after that.

The price is per document, not per page: a 2-page leaflet and a 600-page manual are one billed document each, and a run that finds 40 usable PDFs among 50 addresses bills 40 documents. The price per document is the one shown at the top of this page.

### Input

| Field | Meaning | Default |
|---|---|---|
| `urls` | PDF addresses (http or https). Strings or `{ "url": "…" }`. | required |
| `outputFormats` | `text`, `markdown` or both. | both |
| `includePages` | Add `pages[]` with the text of every page, and `<!-- page N -->` markers in the Markdown. | `false` |
| `pageRange` | For example `1-5,8` or `3-`. Empty = all pages. | all |
| `maxPagesPerDocument` | At most this many pages per PDF (0 = all; hard limit 5,000). | `0` |
| `keepHeadersFooters` | Keep what is normally dropped: lines repeated at the top or bottom of at least half of the pages (page numbers included) and running heads of a chapter (the same line on 5 or more pages in a row, or on every other page; a head that carries the page number, such as `12 Chapter 3. Functions` or `1.3. The first program 3`, counts when its number follows the page number on 5 or more pages). Text turned against the page that repeats on half of the pages (margin stamps) is dropped too. A line of five cells or more is the header of a table that goes on over the pages, not a running head: it is kept. Never dropped in documents of fewer than 4 pages. | `false` |
| `onlyChanged` | Remember each PDF between runs; unchanged ones come back as a free `unchanged` record. | `false` |
| `maxPdfMegabytes` | Larger files are not read (the download is stopped as soon as the limit is passed). Up to 100 (a PDF is held in memory while it is read: see Limits). | `50` |
| `respectRobotsTxt` | Check the robots.txt of the site every PDF, and every redirect, comes from. | `true` |
| `requestsPerSecond` | Politeness limit per host (1–5). A Crawl-delay in robots.txt can only slow it down. | `2` |
| `maxConcurrency` | PDFs processed at the same time (1–16). Each PDF is held in memory while it is read. | `4` |
| `timeoutSecs` | Time limit per PDF, download plus extraction. | `60` |

A PDF whose permission flags say that copying is not allowed is read the way a viewer reads it, and `warnings` says so.

There is no password field, no proxy, no login and no cookie support. PDFs that need any of these are reported as such and are not read.

### Output

One dataset record per input address. Records are stored in the order the PDFs finish, not in the order of the input: use `url` to match them. Every field is always present (`null` when it does not apply).

```json
{
  "url": "https://www.rfc-editor.org/rfc/rfc9309.pdf",
  "finalUrl": "https://www.rfc-editor.org/rfc/rfc9309.pdf",
  "status": "ok",
  "error": null,
  "charged": true,
  "httpStatus": 200,
  "fileName": "rfc9309.pdf",
  "sizeBytes": 177688,
  "sha256": "149bf3e7f8fa21eaab3cfacf425e624cc44aece2547f65b2f43b97d70511ef0d",
  "pageCount": 12,
  "pagesExtracted": 12,
  "pagesWithText": 12,
  "text": "…",
  "markdown": "…",
  "charCount": 22644,
  "wordCount": 3032,
  "language": null,
  "title": "RFC 9309: Robots Exclusion Protocol",
  "titleSource": "metadata",
  "subject": "This document specifies and extends the \"Robots Exclusion Protocol\" method originally defi…",
  "keywords": null,
  "producer": "WeasyPrint 56.1",
  "creationDate": "2022-09-10T00:39:45.000Z",
  "creationDateRaw": "D:20220909173945-07'00'",
  "pdfVersion": "1.7",
  "isTagged": false,
  "outline": [
    { "title": "RFC 9309", "level": 1, "page": 1 },
    { "title": "Robots Exclusion Protocol", "level": 1, "page": 1 },
    { "title": "Abstract", "level": 2, "page": 1 }
  ],
  "links": ["https://www.rfc-editor.org/rfc/rfc9309", "https://www.rfc-editor.org/info/rfc9309"],
  "hasForms": false,
  "likelyScanned": false,
  "truncated": false,
  "fullTextKey": null,
  "warnings": [],
  "change": null
}
```

(Record from a real run, shortened where marked with `…`; the outline and links lists are cut to a few entries. `etag`, `lastModified`, `extractedAt`, `modificationDate`, `modificationDateRaw` and `pages` are also in every record.)

The beginning of the Markdown of the same document:

```markdown
### Abstract

This document specifies and extends the "Robots Exclusion Protocol" method originally defined by Martijn Koster in 1994 for service owners to control how content served by their services may be accessed, …
```

#### Status

| `status` | Meaning | Charged |
|---|---|---|
| `ok` | Text extracted. | yes |
| `partial` | Text extracted, but some pages could not be read or have no text layer (see `warnings`, `pagesWithText`). | yes |
| `no_text` | No text layer: probably a scan. No OCR is done. | no |
| `unreadable_text` | The file has a text layer, but it is not text: fewer than 30 % of its characters are letters or digits, or most are replacement/private-use characters, or 5 % or more of its characters are control codes (U+0000) that the font gives in place of letters, so words come out with holes (typically fonts without a Unicode map; the same rubbish comes out of any PDF tool). The text is delivered as it is in the file, with an explanation in `error`. No OCR is done. | no |
| `encrypted` | Needs a password. | no |
| `not_pdf` | The URL returned something that is not a PDF (the file is recognised by its first bytes, not by Content-Type). | no |
| `too_large` | Over `maxPdfMegabytes`. | no |
| `blocked` | robots.txt forbids it, the server refuses the bot (401, 403, 429, a challenge page) or the address leads to a private network. | no |
| `unreachable` | DNS, connection, timeout or an HTTP error. | no |
| `invalid` | Not a usable web address, or a damaged PDF. | no |
| `unchanged` | Same as in the previous run (`onlyChanged`). | no |

`title` is the title of the PDF's document information; when the file has none (or only a junk one: question marks, a number, a file name) it is the biggest text of the first of the first two pages of the document that has text bigger than the rest, with the lines of one title joined (a `pageRange` that leaves them out gives no title); if that text is the head of a section ("Contents", "Abstract", "1 Introduction") there is no title, and `titleSource` says which one it is (`metadata`, `heading`, or `null` when there is no title). Titles are delivered as the file has them: a file that carries a wrong title (a repeated word, a file name) is reported with it.

`likelyScanned` is `true` when pages that contain images have little or no text. A page that is one picture over the whole sheet with only a few words of text on top (a scanner stamp such as "Scanned by CamScanner", a date, a footer: up to 120 characters) counts as a scanned page; when most pages are like that, the PDF is `no_text` and free, and `error` says that it looks like a scan. `language` is only filled when the PDF declares it; it is never guessed. The author field of the PDF is deliberately not read, because it is usually a person's name; `producer` (the software that made the file) is.

#### Long documents

If text and Markdown together exceed about 3 million characters, the record keeps the first part (cut at a paragraph boundary, about 1 MB per field), sets `truncated: true`, and the complete text is saved in the run's default key-value store under `fullTextKey` (`fulltext-<16 characters of the SHA-256>.md` or `.txt`). No record is ever allowed to exceed the dataset limit.

#### Run summary

The key-value store holds a `SUMMARY` record: totals per status, how many PDFs were charged, how many were skipped and why (spending limit, run timeout), duplicates ignored, number of requests, and `finishedAt`. It is written while the run goes on, so it exists even if the run is stopped.

### How the text is rebuilt

- **Reading order.** Words are placed by their position on the page (page rotation included). Columns are recognised where a vertical channel runs through lines of prose, so a table that sits in one column of a two-column page usually stays in its column and the prose of the other column stays prose (not when the page has tables in both columns, or equations that stand beside the text of the other column: see the limits); two tables side by side are two tables; a table that runs across the page is not read column by column. Rows made only of sentence-like cells (prose in three justified columns) are read column by column instead of across. A column of running text beside an equation, a figure's labels, a table or a list of references in the other column of the page (the sentence goes on from line to line, in full lines) is never a column of a table: the columns are read one after the other, and what is a table in the other column stays a table.
- **Words and lines.** Spaces are restored from the gaps between words. A word broken with a hyphen at the end of a line is joined (`exam-` + `ple` becomes `example`), while real hyphens stay (`state-of-the-art`, `Baden-Württemberg`). The document's own vocabulary decides doubtful cases. Ligatures (ﬁ, ﬂ…) are expanded; control characters are removed.
- **Look-alike characters are normalised, nothing else is.** Compatibility forms that break search are turned into the real letters: Arabic presentation forms (a search for `مصر` finds it, lam-alef is two letters again), Kangxi and CJK radicals (`⽇本` becomes `日本`), CJK compatibility ideographs, Latin, Hebrew and Armenian presentation forms. The rest of the text is not touched: full-width digits and punctuation of Japanese text, superscripts, fractions and symbols stay as they are. Persian/Urdu letter variants that a font uses for Arabic text (ی, ھ) are delivered as the file has them.
- **Table-of-contents leaders.** Dotted leaders (`. . . . . .`, `........`, `·······`: four or more dots in a row) are left out; "Introduction . . . . 3" becomes "Introduction 3". Three dots (an ellipsis) stay.
- **Paragraphs.** One blank line between paragraphs, not a line break at every line. A sentence that continues in the next column or on the next page is joined. A side bar next to the body (a CV with contacts, skills and languages in a narrow column) is read first, then the body, as long as the lines of the bar do not sit on the baselines of the body; a bar floated into a body that wraps around it can still be mixed with it. A reference set as a raised `[10]` stays at the end of its sentence (`metropolis.[10] It was`), also when it wrapped to the next line. One-line entries set with space between them (a CV, an invoice, a list of clauses) stay separate paragraphs; double-spaced prose whose lines reach the right margin stays one.
- **Markdown.** Headings are deduced from font size and weight compared with the body text (up to four levels). Bold is recognised from the font name (Bold, Semibold, Demi, Black, Heavy, Bd, TB…): pdf.js gives no weight for a font. When the PDF has an outline (bookmarks), every entry found on its page becomes a heading with the level of the outline, also when nothing else tells it from the body text, and a title is never glued to the paragraph after it. Lists: in the `text` every marker stays as the document wrote it (`(3)`, `3)`, `(a)`, `iv)`), so a clause can still be quoted; in the Markdown a numeric marker (`(3)`, `3)`, `3.`) becomes an ordered item with its true number (`3.`), and letters and Roman numerals (`(a)`, `b)`, `(iv)`), which Markdown cannot number, become a bullet that keeps the marker (`- (a) text`). Links (`[text](url)` where a link annotation covers the text), and fenced code blocks for monospace text.
- **Tables are best effort.** Rows of at least three lines with clearly aligned columns become Markdown tables; anything less regular stays as text. What counts:
  - **What the page draws.** Rules between rows, a box behind the header, zebra stripes or a grid mark the rows and the columns. A band between two rules is one row when a cell wraps in it, and several rows when each of its lines holds its own values (the rules of a paper separate groups of rows, not rows). A row of header words right over such rows is never a heading.
  - **Values.** A number with a unit, an exponent (`10⁻³`), a fraction (`7/14`), a range (`83%–90%`), `<0.1%`, a dash for a missing value, si/no are values. A word where the table has numbers is the second line of a wrapped cell. A number in brackets under a number (`21.7` over `(71.1)`) is the same cell.
  - **Columns.** A header over two columns (`BLEU` over EN-DE and EN-FR) joins none of them. Cells run together by a narrow gap (`€ 70,00 € 1.680,00`) are cut where the rows around them leave a gutter; two numbers set apart by more than a space are two cells; an exponent or a footnote mark stays in the cell of its number; a value and its uncertainties set above and below it are one cell.
  - **Rows stay whole.** The cells of one line are never cut apart, also for the last rows of a table followed by text. A header over the columns, a description that wraps (also one centred between the numbers of its row) and an invoice of any number of rows come out as ONE table. Booktabs tables (rules over and under the header and at the foot) and totals under a table come out as rows too. The title over a table and the notes under it stay text.
  - **Superscripts and subscripts.** Superscripts inside a sentence stay where they are (`[RWC+19]`; the marks of notes `§ ¶ † ††` stay with their word). A raised number is an exponent, written with Unicode superscript characters in `text` and `markdown` alike (`10⁸`, `m²`, `10⁻³`), never glued to its number; a lowered digit or run of letters (`H2O`, `NO2`, `NOx`, `kBT`) is joined to its word as plain characters; a raised list touching its word (`review¹²,¹³`, `s⁻¹`) is joined too.
  - **Rules over the header and under the last row only.** A table drawn with a rule over and under its header and one at the foot keeps its columns even when they are set closer than a word gap, its cells that wrap (a team and a score centred between the two lines of its institution, or a cell of three lines set at the top: the lines between two rows go with the row they stand closer to) stay in their row, and the heading over it is not part of it. The last rows of a table in one column of a page of two stay in it, and the caption or the note under it ends it.
  - **Tables of results without rules** (labels, estimates, standard errors in brackets or parentheses under them, panels `A.` and `B.`): the header over the columns is the first row, a panel title is a row of its own, the names of columns run together by a narrow gap are cut where the rows below leave a gutter (up to eight of them when every space of the header is one). A number in brackets alone (`(1)`, `(12)`) is still a note, not a value.
  - **Not tables.** Prose set in three justified columns is read column by column.
  - **Limits.** Amounts wrapped inside their cell, header words that wrap, and tables with no rules and numbers set closer than a word gap can still come out with two columns joined. A table made only of words, with no numbers, is cut into rows only where the page draws a rule between rows. A value with its uncertainties set above and below it is one cell, but the plus and minus signs of some fonts come out as another character. Equations with matrices can look like tables. Two-column pages with a table in each column, or a table beside the caption of the table of the other column, come out as rows whose cells hold pieces of the captions and of the text next to them (the Tables 1 and 2 of 1305.6646, Tables 5 and 6 of 1606.05250, the results tables of 1603.01360), and so does a grid of small tables with their captions beside each other (the `(a)` to `(e)` tables of Mask R-CNN, Table 2). Display equations of one column (fractions, sums with limits, several lines high) beside the lines of text of the other column can come out as rows of a table with the equation in some cells and the text in the others; the pages with many of them are the worst (1305.0006, 11 rows; most of the 32 rows with sentence cells of 1305.6646 are in its mathematical sections), and the axes of charts and display equations can come out as small tables. In all these cases the words are all there, but the order inside those rows is not the reading order. A table that fills a whole page with long descriptions in small type (a list of teams with their institutions and authors) comes out one line per row, not always as a Markdown table, and never column by column. The first table of a paper whose numbers stand inside the caption text (the LIGO detection paper, Table I) is still read as text. Do not rely on table structure for forms, merged cells or tables without aligned columns.
- **Right-to-left and CJK.** Right-to-left text is delivered in logical order (also when the generator set it in a monospace font such as FreeMono: Arabic and Hebrew are never put in a code block), and no spaces are added between Chinese and Japanese characters. Arabic letters that a font draws with no width (the hamza of lam-alef) stay in their word, and brackets that the file draws mirrored come out in logical order (`(29)`, not `)29(`). Words in Latin letters inside Arabic text keep their left-to-right order. Limit: a line that mixes Arabic and Latin text is put in order piece by piece from where the pieces sit on the page; in a bibliography line with a few Arabic words in an English entry, the order of the pieces can still differ from the order a reader follows, and a bracket pair split between two pieces of the line may stay mirrored. Korean is written with spaces between words, so the spaces of the file are kept. The lines of one Chinese or Japanese paragraph are joined without a space or a blank line, also when the source is double-spaced; the lines of Korean text are joined with a space. CJK text is never put in a code block because its font has glyphs of equal width: a code block needs a monospace font by name (Courier, Consolas…) or code-like text (almost all ASCII, with letters).
- A text turned against the page (margin stamps) is kept as separate lines, and a turned line that comes back on at least half of the pages ("This publication is available free of charge from…" on every page of a NIST publication) is a stamp: it is dropped like a running head, wherever it stands on the page, unless you keep headers and footers. Diagonal text (watermarks) is left out and counted in `warnings`.
- **Invisible text and layouts that cannot be rebuilt.** Text drawn at a size no reader could see (the `<latexit sha1_base64=…>` blobs that figures made with LaTeXiT or Keynote carry, set at a millionth of a point) is not content and is left out. If the layout of one part of a page cannot be rebuilt, that part is delivered as plain lines (words split at the end of a line stay split, no columns or tables) and `warnings` names the pages; if it cannot be done for the whole document, or the Markdown cannot be built, text and Markdown are the plain lines of every page, with a warning. A PDF that can be read is never lost to its layout.

### Politeness and safety

- Honest identity: the User-Agent names the bot and links to this Actor's page. Nothing is done to get around a refusal; walls and challenges are reported as `blocked`.
- robots.txt is read once per origin, before the first request there, for every PDF and every redirect hop. A `Retry-After` of up to 30 seconds after a `429` is honoured with a single retry.
- Addresses that point to private or local networks are refused, also after redirects. Credentials written inside a URL are removed and never sent, stored or logged.
- The PDF is parsed as data: no JavaScript of the PDF is run and no font or resource is fetched from the network.

### Limits

- **No OCR.** Scans are not read.
- **Tables are best effort** (see above).
- **Password-protected PDFs** are not read, and there is no way to give a password.
- **Forms are not a use case.** Fillable and printed forms (tax returns, applications) have labels, boxes and numbered lines that do not follow a reading order: the text is all there, but the order, the headings and the tables it infers can be wrong. Simple one-column forms read well; for complex ones check a sample first.
- Complex layouts (posters, slide decks, text over drawings; charts and figures next to the text can mix their captions with the body) can come out in an order that is not the visual one; a PDF with a wrong text mapping inside the file can contain stray characters, and they are delivered as they are in the file. When such a file is mostly rubbish it is reported as `unreadable_text` and not charged; when only some pages are, they are named in `warnings`; when 2 % to 5 % of the characters are control codes in place of letters, a warning gives the share.
- The `pageRange` and `maxPagesPerDocument` limits refer to the pages of the PDF, not to printed page numbers.
- Memory grows with the largest PDF you allow, because each PDF is held in memory while it is read, and `maxConcurrency` PDFs are read at the same time. In a measured run (ten documents, the largest 194 pages and 2.8 MB) the peak was about 420 MB; four PDFs of 143 MB read together needed about 1.5 GB, more than the default 1,024 MB of memory. That is why `maxPdfMegabytes` stops at 100: with files near that size, lower `maxConcurrency` or raise the memory of the run.

### FAQ

**Is the file stored?** No. It is read in memory and dropped. Only the record is stored.

**What does `onlyChanged` remember?** For each address: ETag, Last-Modified and the SHA-256 of the file, plus a fingerprint of the options that shape the record. Only what was delivered in a run is remembered. When the server supports conditional requests the file is not downloaded again. If you change options such as `pageRange` or the formats, the record is made again, and `change` still says `unchanged` if the file is the same.

**Why is a page of a PDF I can read in a viewer reported as having no text?** Because it is an image of text. Run it through an OCR tool first.

**Why did I get `blocked`?** The site's robots.txt forbids the path for this bot, or the server answered 401, 403 or 429, or served a challenge page. You can turn `respectRobotsTxt` off for files you are entitled to fetch; refusals by the server itself are never worked around.

**Can I get the text per page?** Yes: `includePages`.

# Actor input Schema

## `urls` (type: `array`):

Addresses of PDF files (http or https). One record per address. Addresses that are not usable web addresses, or point to private/local networks, get an "invalid" record (free), not a run error. Credentials written inside a URL are removed and never used.

## `outputFormats` (type: `array`):

"text" is clean plain text, "markdown" adds headings, lists, tables (best effort), links and code blocks. Both by default.

## `includePages` (type: `boolean`):

Add pages\[] to each record with the text of every page, and  markers in the Markdown.

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

For example 1-5,8 or 3- (from page 3 to the end). Empty = all pages.

## `maxPagesPerDocument` (type: `integer`):

Read at most this many pages of each PDF (after the page range). Hard limit 5,000.

## `keepHeadersFooters` (type: `boolean`):

By default the lines that repeat at the top or bottom of at least half of the pages (page numbers included), and the running heads of a chapter (the same line on 5 or more pages in a row, or on every other page, or whose page number is part of the line: "12 Chapter 3. Functions"), and margin stamps turned against the page that repeat on half of the pages, are left out of the text and the Markdown. Turn this on to keep them. Never applied to documents of fewer than 4 pages.

## `onlyChanged` (type: `boolean`):

Remember each PDF between runs (ETag, Last-Modified and a SHA-256 of the file). A PDF that has not changed gets a free "unchanged" record without text, and when the server supports it the file is not even downloaded. Only what was delivered is remembered.

## `maxPdfMegabytes` (type: `integer`):

Larger files are not read (free, status too_large). The download is stopped as soon as the limit is passed. Every PDF is held in memory while it is read (several times its size) and several are read at once: up to 100 MB fits the default memory of 1,024 MB with a low "PDFs at the same time"; raise the memory of the run before you raise the concurrency.

## `respectRobotsTxt` (type: `boolean`):

Check the robots.txt of the site every PDF (and every redirect) comes from. PDFs it forbids are not downloaded (free, status blocked).

## `requestsPerSecond` (type: `integer`):

Politeness limit for each host. A Crawl-delay in robots.txt can only slow it down.

## `maxConcurrency` (type: `integer`):

Each PDF is held in memory while it is read. With very large files, lower this or raise the memory of the run.

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

Download plus extraction. Also the longest silence allowed while a file arrives.

## Actor input object example

```json
{
  "urls": [
    "https://www.rfc-editor.org/rfc/rfc9309.pdf",
    "https://www.irs.gov/pub/irs-pdf/fw9.pdf"
  ],
  "outputFormats": [
    "text",
    "markdown"
  ],
  "includePages": false,
  "maxPagesPerDocument": 0,
  "keepHeadersFooters": false,
  "onlyChanged": false,
  "maxPdfMegabytes": 50,
  "respectRobotsTxt": true,
  "requestsPerSecond": 2,
  "maxConcurrency": 4,
  "timeoutSecs": 60
}
```

# Actor output Schema

## `documents` (type: `string`):

No description

## `content` (type: `string`):

No description

## `problems` (type: `string`):

No description

## `summary` (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 = {
    "urls": [
        "https://www.rfc-editor.org/rfc/rfc9309.pdf",
        "https://www.irs.gov/pub/irs-pdf/fw9.pdf"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("far003/pdf-to-text-markdown-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 = { "urls": [
        "https://www.rfc-editor.org/rfc/rfc9309.pdf",
        "https://www.irs.gov/pub/irs-pdf/fw9.pdf",
    ] }

# Run the Actor and wait for it to finish
run = client.actor("far003/pdf-to-text-markdown-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 '{
  "urls": [
    "https://www.rfc-editor.org/rfc/rfc9309.pdf",
    "https://www.irs.gov/pub/irs-pdf/fw9.pdf"
  ]
}' |
apify call far003/pdf-to-text-markdown-extractor --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,far003/pdf-to-text-markdown-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/Om07MhEtmt5jIj5Bm/builds/A8M2dtQcAaAFG4enM/openapi.json
