# PDF to Markdown & Text Extractor (`lintlab/pdf-to-markdown`) Actor

Extract page-aware Markdown or text from public PDFs, including metadata, links, simple tables, and image-only page flags. Built for RAG and AI agents; no OCR for scanned text.

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

## Pricing

$3.00 / 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.

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 Markdown & Text

Convert public text-layer PDFs into page-aware Markdown or plain text for search, RAG preprocessing, and AI-agent workflows. One dataset item is written per input URL.

### What you get

- Markdown page markers, inferred headings, obvious lists, and preserved links
- Best-effort simple Markdown tables with alignment confidence
- PDF metadata, page counts, hashes, and structured link records
- Image-only page detection with explicit warnings
- $0.003 per successfully processed PDF

**No OCR:** scanned or image-only pages are detected, but their text is not extracted.

### Quick start

```json
{"pdfUrls":[{"url":"https://example.com/report.pdf"}],"outputFormat":"markdown","includeTables":true}
```

### Use with AI agents / MCP

Call `lintlab/pdf-to-markdown` through the Apify API or Apify MCP server, then read `markdown`, `perPage`, `metadata`, and `imageOnlyPages` from the default dataset before sending text to an LLM or indexer.

### Overview

Convert public PDF URLs into page-aware Markdown or plain text with a pure JavaScript Apify Actor. It downloads each document safely, respects `robots.txt`, verifies that the response is a PDF, and writes one dataset item per URL.

### What it does

- Extracts text without a browser or native PDF binary.
- Marks every Markdown page with `<!-- page N -->`.
- Infers headings from relative font sizes and preserves obvious lists.
- Converts consistently aligned text into simple Markdown tables and reports a best-effort confidence score.
- Converts link annotations into Markdown links and returns a structured link list.
- Returns document title, author, creator, producer, creation date, and modification date when embedded in the PDF.
- Reports pages with little or no extractable text as image-only instead of failing.
- Uses SSRF-safe downloads with DNS pinning, public-unicast-only addresses, redirect checks, byte limits, timeouts, and fail-closed robots handling.

PDFs are processed with concurrency 2. Password-protected or encrypted PDFs are reported as `encrypted`; the Actor never attempts to bypass encryption, DRM, paywalls, or logins.

### Input

```json
{
  "pdfUrls": [
    { "url": "https://example.com/report.pdf" }
  ],
  "maxPdfs": 50,
  "maxPagesPerPdf": 200,
  "maxPdfBytes": 26214400,
  "includeTables": true,
  "outputFormat": "markdown",
  "timeoutSecs": 30
}
```

`outputFormat` accepts `markdown`, `text`, or `both`. Duplicate URLs are removed. `maxPdfs` limits unique input URLs, and the run's maximum charge setting can lower the number attempted.

### Output

Each input produces one dataset item, including failures:

```json
{
  "url": "https://example.com/report.pdf",
  "finalUrl": "https://cdn.example.com/report.pdf",
  "status": 200,
  "bytes": 48192,
  "sha256": "54eaf...",
  "pageCount": 2,
  "pagesProcessed": 2,
  "metadata": {
    "title": "Quarterly report",
    "author": "Example organization",
    "creator": null,
    "producer": null,
    "creationDate": null,
    "modDate": null
  },
  "markdown": "<!-- page 1 -->\n\n# Quarterly report\n\n...",
  "perPage": [
    { "page": 1, "chars": 831, "imageOnly": false },
    { "page": 2, "chars": 0, "imageOnly": true }
  ],
  "imageOnlyPages": [2],
  "links": [
    { "page": 1, "text": "Source", "url": "https://example.com/source" }
  ],
  "tables": 1,
  "tableConfidence": [
    { "page": 1, "rows": 3, "columns": 2, "confidence": 1 }
  ],
  "warnings": ["Pages 2 contain little or no extractable text and may be image-only. OCR is not supported."],
  "error": null
}
```

The default key-value store also contains `SUMMARY`, a JSON run summary with processed, failed, charged, page, byte, and error counts.

### Pricing

The Actor costs **$0.003 per successfully processed PDF**, or **$3 per 1,000 PDFs**. The dataset item is saved before charging. Failed downloads, robots-blocked URLs, non-PDF responses, oversized responses, parse failures, and encrypted PDFs are free. A clean image-only report is successful and chargeable because it identifies affected pages without failing.

### Limits and responsible use

This first version does not perform OCR. Scanned or image-only pages are identified but have no extracted text. Heading, paragraph, list, link-label, and table reconstruction are geometry-based heuristics; complex layouts, multi-column prose, forms, and nested tables may not reproduce perfectly. Table detection is best effort, and its confidence measures alignment rather than semantic correctness.

Only process documents you own or are authorized to access and transform. The Actor honors `robots.txt`, accepts only public HTTP(S) destinations, and does not authenticate or bypass access controls.

***

Built by **lintlab** — small, reliable data tools. AI-assisted (Claude/Codex), reviewed before release.
Support: lintlab.dev@gmail.com · https://github.com/lintlab

# Actor input Schema

## `pdfUrls` (type: `array`):

HTTP or HTTPS URLs of PDF documents. robots.txt is checked for every origin, including redirect targets.

## `maxPdfs` (type: `integer`):

Maximum number of unique PDF URLs to attempt. The run charge limit may reduce this value.

## `maxPagesPerPdf` (type: `integer`):

Extract only the first N pages of each document. The total page count is still reported.

## `maxPdfBytes` (type: `integer`):

Hard streaming download and decompression limit per PDF. Default is 25 MiB (26,214,400 bytes).

## `includeTables` (type: `boolean`):

Convert consistently aligned text rows into Markdown tables and report confidence. This is best effort.

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

Return Markdown, plain text, or both. Markdown includes explicit page boundary comments.

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

Timeout applied independently to robots.txt and PDF HTTP requests.

## Actor input object example

```json
{
  "pdfUrls": [
    {
      "url": "https://www.w3.org/WAI/ER/tests/xhtml/testfiles/resources/pdf/dummy.pdf"
    }
  ],
  "maxPdfs": 50,
  "maxPagesPerPdf": 200,
  "maxPdfBytes": 26214400,
  "includeTables": true,
  "outputFormat": "markdown",
  "timeoutSecs": 30
}
```

# Actor output Schema

## `dataset` (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 = {
    "pdfUrls": [
        {
            "url": "https://www.w3.org/WAI/ER/tests/xhtml/testfiles/resources/pdf/dummy.pdf"
        }
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("lintlab/pdf-to-markdown").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 = { "pdfUrls": [{ "url": "https://www.w3.org/WAI/ER/tests/xhtml/testfiles/resources/pdf/dummy.pdf" }] }

# Run the Actor and wait for it to finish
run = client.actor("lintlab/pdf-to-markdown").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 '{
  "pdfUrls": [
    {
      "url": "https://www.w3.org/WAI/ER/tests/xhtml/testfiles/resources/pdf/dummy.pdf"
    }
  ]
}' |
apify call lintlab/pdf-to-markdown --silent --output-dataset

```

## MCP server setup

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

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/Ov5GMUqzEh2ZUEmck/builds/J286iIsi7IPI605N1/openapi.json
