# PDF to Markdown Converter: Text, Tables, CSV (`kelp_forest_labs/pdf-to-markdown`) Actor

Convert PDFs to clean Markdown with headings, lists and tables, plus plain text and one CSV per table. Public URLs or uploaded files, up to 2,000 pages each. Scanned pages are reported, not billed, and unreadable files are free error rows. Built for RAG and AI agents.

- **URL**: https://apify.com/kelp_forest_labs/pdf-to-markdown.md
- **Developed by:** [kelp forest](https://apify.com/kelp_forest_labs) (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 $4.00 / 1,000 document converteds

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

Give it PDF links (or upload a PDF). For each document you get **clean Markdown** (headings, paragraphs, bullet and
numbered lists, tables), the **plain text layer**, and **every ruled table as rows and as a CSV file**.

### Use cases

- Prepare reports, manuals and papers for RAG and vector databases.
- Pull tables out of PDFs into CSV for spreadsheets.
- Give AI agents readable text from PDF links.
- Bulk-convert document archives to Markdown for search.

### What it does

- Rebuilds headings from font sizes (`#`, `##`, `###`), re-joins wrapped lines into paragraphs and turns bullets
  into Markdown lists.
- Detects ruled tables, renders them as Markdown tables (pipes escaped) and keeps their cells out of the paragraph text.
- Saves the full `.md`, `.txt` and one `.csv` per table in the run's key-value store, with links in each row.
- Reports pages that have no text layer (scans), so you know to send them to OCR instead.
- Detects regular column layouts (papers, newsletters) and reads them column by column, left to right, keeping
  full-width titles in place. Two-column pages are listed in `twoColumnPages` and pages with three or more columns
  in `multiColumnPages`, each with a warning, because the order around figures, tables, footnotes and text that
  spans only some columns may still need a check. Pages where text sits side by side but does not look like
  regular columns (a sidebar, a short last page, or a possible borderless table whose cells line up row by row) are
  not reordered; they are listed in `mixedLayoutPages` with a warning so you know to check them.
- Accepts public URLs, Apify start URLs, or a file uploaded in the Console.

### Pricing (pay per event)

| Event | Price |
|---|---|
| `document` (one PDF converted) | US$0.004 |
| `page` (one converted page with a text layer) | US$0.0002 |

Example: a 10-page PDF costs US$0.004 + 10 × US$0.0002 = US$0.006; a 50-page PDF costs US$0.014.
**Failures are free:** files that are not PDFs, password-protected or damaged PDFs, downloads answering HTTP 400 or
higher, blocked targets, and scanned PDFs with no text layer produce an error row and no charge. Pages beyond
`maxPagesPerDocument` and pages with no text layer are not billed.

Each run also has Apify's run-start charge of US$0.00005 per GB of run memory (US$0.00005 at the default 1024 MB),
charged when the run starts, even if every document fails.

**Maximum charge per run:** before each document, the actor checks that the document fee fits in what is left of your
maximum charge; if it does not, the PDF is not downloaded and you get a free row saying so. The document fee is
charged when the row is saved, then the pages that still fit; if only some pages fit, the full text is delivered,
`pagesCharged` says how many were billed, and the run stops after that document.

### How to use

1. Open the actor in Apify Console and fill in the input form (or paste the JSON example below into the JSON editor).
2. Optional: set a maximum charge for the run under the run options to cap what a run can cost.
3. Click **Start**. Results appear in the **Output** tab as they are produced; export them as JSON, CSV, Excel or HTML.

You can also start runs from the Apify API or CLI, run the actor on a schedule (for example weekly), and send
results to other apps with Apify integrations and webhooks.

### Input example

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

### Output example (local fixture run, Oct 7, 2026; trimmed)

```json
{"url": "https://example.com/report.pdf", "status": "ok", "title": "Quarterly Fixture Report", "pages": 2, "pagesTotal": 3, "pagesCharged": 2,
 "markdown": "# Quarterly Fixture Report\n\nThis paragraph explains ...\n\n## Key points\n\n- First point about revenue\n- Second point about costs\n\n| Region | Q1 | Q2 |\n|---|---|---|\n| BC | 120 | 135 |\n...",
 "tables": [{"page": 1, "index": 1, "rows": [["Region", "Q1", "Q2"], ["BC", "120", "135"]], "csvUrl": "https://api.apify.com/v2/key-value-stores/<store-id>/records/report-....csv"}],
 "tableCount": 1, "warnings": ["only the first 2 of 3 pages were converted (maxPagesPerDocument)"], "charges": {"document": 1, "page": 2}}
```

### FAQ

**Does it do OCR?**
No. Pages without a text layer (scans) are listed in the output and not billed; use an OCR tool for those.

**How are tables detected?**
Tables with ruling lines are detected and exported as Markdown and CSV. Tables without lines are not exported as tables: their text is read across the page, row by row. If the cells hold wrapped text, the page gets a 'possible borderless table' warning and is listed in `mixedLayoutPages`.

**What does a 10-page PDF cost?**
US$0.004 + 10 x US$0.0002 = US$0.006. A 50-page PDF costs US$0.014. Each run adds Apify's run-start charge
(US$0.00005 per GB of memory, US$0.00005 at the default 1024 MB).

**Are my files kept?**
Only in your own run storage, under your account's data retention.

**Does it handle two-column layouts?**
Yes, for regular column layouts (papers, newsletters): the text is read column by column, left to right, and the row gets a warning plus `twoColumnPages` or `multiColumnPages` (three or more columns) so you can check the order around figures, tables and footnotes. Text that sits side by side but does not look like regular columns (a sidebar, or a borderless table whose cells line up row by row) is not reordered; those pages are listed in `mixedLayoutPages` with a warning.

### Limits: what this actor does not do

- No OCR: scanned pages without a text layer are reported, not read.
- No password cracking: protected PDFs are skipped (free).
- Tables without ruling lines are not exported as tables. Their text is read row by row, so a row stays together,
  but the lines of wrapped cells in one row can be interleaved; such pages get a "possible borderless table"
  warning in `mixedLayoutPages`. Sidebars and text spanning only some columns are flagged in `mixedLayoutPages`
  too and not reordered.
- A borderless table may still be read column by column (and labelled as columns) if almost every cell (about 70%
  or more) ends like a sentence (. ? ! : ;) and, in every row, all cells have the same number of lines, because it
  then looks exactly like columns of evenly spaced paragraphs.
- Borderless tables with very tight rows (0-1 pt row padding) or vertically centred cells may still be read as
  columns. So may a page that holds only two rows of a table, whether it is a stand-alone 2-row table or the last
  page of a longer one.
- Each download must finish within 2 minutes (a server that sends data very slowly gets a free error row, and
  the next PDF goes ahead).
- No private, loopback or cloud-metadata addresses (an SSRF guard checks every redirect hop). Files are kept only in
  your run's storage.

### Changelog

- 1.0 (October 2026): first public version.

# Actor input Schema

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

Public http(s) links to PDF files. Private, loopback and login-walled links are not supported.

## `pdfFile` (type: `string`):

Upload one PDF from your computer instead of (or as well as) the URLs.

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

Same as PDF URLs, in Apify's start-URL format.

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

Only the first N pages are converted and billed.

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

Detect ruled tables, render them as Markdown tables and save each as CSV.

## `includeText` (type: `boolean`):

Also return the raw text layer (pages separated by a form feed).

## `saveFiles` (type: `boolean`):

Save the full .md, .txt and one .csv per table, with links in the output row.

## Actor input object example

```json
{
  "pdfUrls": [
    "https://www.w3.org/WAI/ER/tests/xhtml/testfiles/resources/pdf/dummy.pdf"
  ],
  "maxPagesPerDocument": 100,
  "extractTables": true,
  "includeText": true,
  "saveFiles": true
}
```

# Actor output Schema

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

One row per PDF with Markdown, text, tables and metadata, or a free error row.

## `files` (type: `string`):

The .md, .txt and per-table .csv files in the run's key-value store (when saveFiles is on).

# 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": [
        "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("kelp_forest_labs/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": ["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("kelp_forest_labs/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": [
    "https://www.w3.org/WAI/ER/tests/xhtml/testfiles/resources/pdf/dummy.pdf"
  ]
}' |
apify call kelp_forest_labs/pdf-to-markdown --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,kelp_forest_labs/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/QIOeFd1cT9BJCcUlC/builds/1DEtRTxP6EeHAgLj5/openapi.json
