# PDF to Markdown, Tables & OCR for RAG (`mzansi-dataworks/pdf-to-markdown-tables-ocr`) Actor

Turn PDFs into clean Markdown, plain text, per-table JSON and RAG chunks with page numbers. OCR runs only on scanned pages. Public URLs or Key-value store files. Pay per page, no subscription.

- **URL**: https://apify.com/mzansi-dataworks/pdf-to-markdown-tables-ocr.md
- **Developed by:** [Norton Claude](https://apify.com/mzansi-dataworks) (community)
- **Categories:** AI, Developer tools
- **Security:** Limited permissions
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

Pay per event

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, Tables & OCR for RAG

Turn PDF files into **clean Markdown, plain text, tables as JSON, and RAG-ready chunks with page numbers**. Paste a link, press Start, get text you can feed to an LLM, a vector database or a spreadsheet.

The Actor reads the text that is already inside the PDF (fast, cheap) and only runs **OCR on the pages that really are scans**. A report with 40 text pages and 2 scanned appendix pages pays the OCR price for 2 pages, not 42.

### What you get

- **Markdown** with headings, paragraphs, lists and GitHub tables, in reading order (columns are not interleaved), with running headers, footers and page numbers removed.
- **Plain text** of the same content, if you prefer it.
- **Every table as JSON** next to the Markdown: `rows` (first row is the header), `records` (one object per row when the header is clean), and the table as a Markdown string, each with its page number.
- **RAG chunks**: pieces of text of the size you choose, each with `pageStart`, `pageEnd`, `pages[]`, the `heading` it sits under and a stable `chunkId`, so every answer can cite "page 12".
- **OCR only where needed** (scans, photographed documents, PDFs with a broken text layer). The page is marked `source: "ocr"` and you get a confidence score.
- **Scanned and text pages mixed in one file** are handled page by page.
- **Passwords** for encrypted PDFs (kept secret, never logged or stored in the output).
- **One bad file never stops the run.** A wrong link, a non-PDF, a locked file or a corrupt PDF becomes one dataset item that says why, and costs nothing.
- **Private files** work with a signed link: upload the PDF to your own Apify Key-value store and paste the record's public link (it carries a signature, so it opens without logging in).

### Who uses it

- Teams building **RAG and AI agents** over contracts, reports, manuals, municipal and government documents.
- Analysts who need the **tables out of PDF reports** (budgets, statistical releases, annual reports) as rows and columns.
- Back-office automation that turns **scanned invoices and forms** into searchable text.
- Anyone who needs a clean Markdown copy of a paper or a report to read, search or summarise.

### How to use it

1. Paste one or more **PDF URLs** (the link must lead straight to the file). The prefilled example works as is.
2. Pick the **output shape**: one item per PDF, one item per page, or RAG chunks.
3. Press **Start**. Download the dataset as JSON, CSV, Excel or HTML, or read it through the API.

Leave everything else on its defaults; the defaults are what most people want.

#### Example inputs

Read a report, get one item with the Markdown and all tables:

```json
{
  "pdfUrls": [{ "url": "https://mozilla.github.io/pdf.js/web/compressed.tracemonkey-pldi-09.pdf" }]
}
```

Chunks for a vector database, 1,200 characters with 150 overlap, only pages 1 to 20:

```json
{
  "pdfUrls": [{ "url": "https://example.com/handbook.pdf" }],
  "outputMode": "chunks",
  "chunkSize": 1200,
  "chunkOverlap": 150,
  "pageRange": "1-20"
}
```

An encrypted PDF, and a file from your own Key-value store (use the record's public link, which includes `?signature=...`):

```json
{
  "pdfUrls": [
    { "url": "https://example.com/locked-statement.pdf" },
    { "url": "https://api.apify.com/v2/key-value-stores/YOUR_STORE_ID/records/scanned-invoice.pdf?signature=YOUR_SIGNATURE" }
  ],
  "password": "the-pdf-password"
}
```

The same password is tried on every PDF in the run.

#### Main options

| Option | What it does |
|---|---|
| `pdfUrls` | Links to PDF files (http or https). One per line, or paste a list. |
| `outputMode` | `document` (one item per PDF), `pages` (one per page) or `chunks` (RAG pieces). |
| `pageRange` | `1-10,15`, `20-` (page 20 to the end), `-5` (first five). You pay only for pages read. |
| `extractTables` | Detect tables (ruled or aligned) and return them as Markdown tables and JSON. On by default. |
| `ocr` | `auto` (scanned pages only), `off`, or `force` (every page). |
| `includeMarkdown`, `includeText` | Which text formats to return. |
| `chunkSize`, `chunkOverlap` | Chunk length and overlap, **in characters** (about 4 characters per English token). |
| `removeHeadersFooters` | Drop lines repeated at the top or bottom of most pages. |
| `maxPagesPerDocument` | Cut very long PDFs (default 500) and mark them `partial`. |
| `maxFileSizeMb` | Refuse bigger files before download completes (default 50, not charged). |
| `password` | For encrypted PDFs. Secret. |

### Output

One dataset item per PDF in the default mode. Real output from a Statistics South Africa release (shortened; the full item also has the whole `markdown`, 12 tables and 20 pages):

```json
{
  "recordType": "document",
  "url": "https://www.statssa.gov.za/publications/P0441/P04412ndQuarter2024.pdf",
  "documentId": "25e71ceff235",
  "title": "Gross domestic product",
  "pageCount": 20,
  "status": "ok",
  "error": null,
  "metadata": { "author": "mariette", "creator": "Microsoft® Word 2016", "createdAt": "2024-09-02T12:28:17+02:00" },
  "pagesRequested": 20,
  "pagesProcessed": 20,
  "textPages": 20,
  "ocrPages": 0,
  "blankPages": 0,
  "tableCount": 12,
  "wordCount": 9773,
  "markdown": "<!-- page 1 -->\n\n## STATISTICAL RELEASE\n\n### P0441\n\n# Gross domestic product\n\n### Second quarter 2024\n\n...\n\n<!-- page 3 -->\n\n## Key findings for the second quarter of 2024\n\nReal gross domestic product (GDP) measured by production, increased by 0,4%¹ in the second quarter of 2024 ...",
  "tables": [
    {
      "id": "p19-t1",
      "page": 19,
      "index": 1,
      "kind": "ruled",
      "rowCount": 9,
      "columnCount": 3,
      "rows": [
        ["Statistical release", "Reason for revision", "Period subject to revision"],
        ["Second quarter 2024", "Updated source data", "First quarter 2024"],
        ["Third quarter 2024", "Updated source data", "Second quarter 2024"]
      ],
      "records": [
        { "Statistical release": "Second quarter 2024", "Reason for revision": "Updated source data", "Period subject to revision": "First quarter 2024" }
      ],
      "markdown": "| Statistical release | Reason for revision | Period subject to revision |\n| --- | --- | --- |\n| Second quarter 2024 | Updated source data | First quarter 2024 |\n..."
    }
  ],
  "warnings": [],
  "charges": { "document": 1, "page": 20, "ocrPage": 0, "totalUsd": 0.013 },
  "fileSizeBytes": 1143899,
  "sha256": "cf6b6eec89d6ae4b08f7a4a24e82c4c93c3fc0496ed29db22649170af13d081b",
  "processingSeconds": 6.5,
  "extractedAt": "2026-10-17T12:11:28+00:00"
}
```

`tables[].kind` is `ruled` (drawn lines) or `aligned` (columns found by position). Each table's `markdown` is also placed in the document Markdown at the place it appears.

#### Chunk mode

Every chunk knows where it came from (real output, one chunk):

```json
{
  "recordType": "chunk",
  "url": "https://www.statssa.gov.za/publications/P0441/P04412ndQuarter2024.pdf",
  "documentId": "25e71ceff235",
  "chunkId": "25e71ceff235-0001",
  "chunkIndex": 1,
  "chunkCount": 5,
  "text": "contributed 0,3 of a percentage point to the GDP growth.\n\nThe trade industry increased by 1,2% and contributed 0,1 of a percentage point. ...",
  "pageStart": 3,
  "pageEnd": 3,
  "pages": [3],
  "heading": "Key findings for the second quarter of 2024",
  "headingPath": ["Key findings for the second quarter of 2024"],
  "charCount": 412,
  "usesOcr": false
}
```

Chunks follow paragraph, list and table boundaries where they can. A table is never cut in the middle of a row when it fits in a chunk. `chunkOverlap` repeats the end of one chunk at the start of the next.

#### Page mode

One item per page with `page`, `source` (`text`, `ocr` or `blank`), `markdown`/`text`, that page's `tables`, `ocrConfidence`, `width`, `height` and `warnings`.

#### Run summary

The key-value store record `SUMMARY` lists every PDF with its status, page counts, tables, seconds and cost, plus totals and `peakMemoryMb`. When a run stops at your cost limit it says so in `stoppedBecause`.

#### Statuses and error codes

`status` is `ok`, `partial` (some pages missing, for example the page limit or your cost limit was reached), `empty` (nothing readable, for example a scan with OCR off), `failed` or `skipped` (not started because the cost limit was reached).

| `error.code` | Meaning |
|---|---|
| `INVALID_URL`, `BLOCKED_URL` | Not an http(s) link, or it points at a private or internal address (refused on purpose). |
| `DNS_FAILED`, `HTTP_404`, `HTTP_403`, `HTTP_xxx`, `TIMEOUT`, `TLS_ERROR`, `TOO_MANY_REDIRECTS`, `DOWNLOAD_FAILED` | The download failed. `retryable` tells you whether trying later may help. |
| `NOT_A_PDF` | The link or record is not a PDF (an HTML page, JSON, an image). |
| `FILE_TOO_LARGE` | Over `maxFileSizeMb`. |
| `PASSWORD_REQUIRED`, `WRONG_PASSWORD` | Encrypted PDF without or with the wrong password. |
| `CORRUPT_PDF`, `EXTRACTION_FAILED` | The file cannot be parsed, or no page could be read. |
| `RECORD_NOT_FOUND`, `STORE_ERROR` | The Key-value store record does not exist or cannot be read. |
| `CHARGE_LIMIT_REACHED` | Status `skipped`: your maximum cost per run was reached first. |
| `INTERNAL_ERROR` | Something unexpected went wrong with this one PDF; the others are not affected. |

A page that could not be read inside an otherwise good PDF is listed in `failedPages` with a code (`PAGE_TIMEOUT` after 90 s, or 180 s when OCR may run; `MEMORY_LIMIT`, `WORKER_CRASHED`, `EXTRACT_FAILED`). Such pages are not charged.

### Pricing

Pay per use, no subscription, no start fee. Three events:

| Event | Price | When |
|---|---|---|
| PDF processed | **$0.003** | Once per PDF that was read successfully. |
| Page read from the text layer | **$0.0005** | Per page whose text was read directly. |
| Page read with OCR | **$0.004** | Per scanned page that OCR actually read. |

Failed PDFs (404, not a PDF, wrong password, corrupt file) cost nothing. Blank pages cost nothing. A page where OCR finds no text costs nothing.

Worked examples (from real runs):

| PDF | Pages | Cost |
|---|---|---|
| Mozilla pdf.js sample paper (the prefilled example) | 14 text | 0.003 + 14 x 0.0005 = **$0.010** |
| Statistics South Africa GDP release | 20 text | 0.003 + 20 x 0.0005 = **$0.013** |
| A 40-page generated report with a table on every page | 40 text | 0.003 + 40 x 0.0005 = **$0.023** |
| Scanned invoice batch file | 3 scans | 0.003 + 3 x 0.004 = **$0.015** |
| Report with 2 text pages and 1 scanned page | 2 text, 1 scan | 0.003 + 2 x 0.0005 + 0.004 = **$0.008** |
| 1,000 PDFs of 10 text pages each | 10,000 text | 1,000 x (0.003 + 10 x 0.0005) = **$8.00** |

Set **Maximum cost per run** in the run options to cap what you spend. The Actor stops cleanly at the limit: PDFs it did not reach are listed as `skipped`, and a PDF that hits the limit halfway is returned as `partial` with the pages already read. It never goes over the limit. With OCR on `auto`, a page is only attempted with OCR when the OCR price still fits in your remaining budget.

### Limits (read this before you rely on it)

- **OCR reads printed text only**: English and Chinese letters, digits and common punctuation. **No handwriting.** No Cyrillic, Greek, Arabic, Hebrew or Indic scripts. Accented Latin letters are often returned without the accent (`e` for `é`). Similar shapes get confused now and then (`0` and `o`).
- **OCR word breaks are rebuilt from the page image**, because the OCR model drops spaces. On clean A4 scans at 150 dpi we measured about 98% of words exactly right and 99%+ character accuracy; typewriter fonts, tiny print and poor scans are worse. Always check numbers that matter.
- **Scanned tables come out as lines of text**, not as table cells. Table structure is recovered from the text layer only.
- **Complex tables are not perfect.** Ruled and clearly aligned tables work well. Merged cells, tables with multi-line wrapped cells, nested headers and tables that continue over several pages can come out fragmented (a wrapped cell may become two rows; page-spanning tables are not joined). Check important tables.
- **Layout is a best effort.** Multi-column text is read column by column, but unusual layouts (magazines, posters, forms with boxes) can come out in the wrong order. Text inside figures, charts and code listings can leak into the running text as noise. On pages where most text is rotated or mirrored (some plots and attention maps) tables are not extracted and a warning is added.
- **Not extracted:** images and their content, form fields, annotations, hyperlinks, vector drawings, PDF bookmarks. Footnote numbers are kept as superscript characters next to the word.
- **Public links only for downloads.** Some sites block automated downloads (bot protection, login walls); when a site answers 403 the Actor retries with a browser-style identity, but it cannot get past a login or a bot challenge. Upload such files to your own Key-value store and paste the record's public link instead.
- **Safety refusals:** links to private networks, localhost and cloud metadata addresses are refused, also after redirects. Only ports 80, 443, 8080 and 8443 are allowed.
- **Size limits:** 50 MB per file by default (up to 200 MB), 500 pages per file by default (up to 5,000).
- **Memory:** text-only runs need about 200 MB. Scanned pages need about 700 MB, so use at least **1024 MB** (the default) for scans and **2048 MB or more** for large scans or long OCR batches. If a page does not fit, that page fails with a clear message and is not charged.
- **Speed:** text pages take about 0.2 to 0.4 s each. OCR takes about 3 s per A4 page on a fast core. Apify gives more CPU to bigger memory settings, so give scan-heavy runs 2048 to 4096 MB if you need them quickly.
- `requestsFromUrl` lists (a link to a text file of links) are not supported; paste the links.

### FAQ

**Does it work on scanned PDFs?** Yes. Leave OCR on `auto`. Pages without a usable text layer (or with a garbled one) are OCR'd, the rest are read directly. Switch to `force` only if you know the text layer of a file is wrong in a way the Actor cannot detect.

**How do I know which pages were OCR'd?** Each page record and each chunk says so (`source`, `usesOcr`), and the document item has `ocrPages` and `ocrConfidence`.

**Why is `title` sometimes the file name?** The title comes from the PDF's own metadata when it looks like a real title, otherwise from the first large heading on the first two pages that were read, otherwise from the file name.

**What happens to my files?** Downloaded files are processed in a temporary folder inside the run and deleted when each document is done. The Actor does not keep your PDFs, does not send them to any other service, and the PDF parser runs in a separate process with no access to your API token.

**Can I read a PDF from Google Drive, Dropbox or S3?** Use a direct download link, or a presigned URL. Links that open a viewer page are not PDFs and return `NOT_A_PDF`. Signed URLs are never written to the log (query strings are removed).

**What are the characters in chunk size?** Characters, not tokens. As a rule 1,500 characters is roughly 375 English tokens.

**Can I run it for thousands of PDFs?** Yes; one run can take thousands of links. Failures are isolated per document. A run that the platform restarts is designed to continue where it stopped and not to charge the same PDF twice.

### Use with RAG and vector stores

Use `outputMode: "chunks"`. Every item is ready to embed, with the metadata most vector stores want:

```python
import os, requests

url = "https://api.apify.com/v2/acts/mzansi-dataworks~pdf-to-markdown-tables-ocr/run-sync-get-dataset-items"
chunks = requests.post(
    url,
    params={"token": os.environ["APIFY_TOKEN"]},
    json={"pdfUrls": [{"url": "https://example.com/handbook.pdf"}], "outputMode": "chunks", "chunkSize": 1200, "chunkOverlap": 150},
    timeout=600,
).json()

for c in chunks:
    if c["recordType"] != "chunk":      # failed or skipped PDFs come back as one "document" item that says why
        print("not read:", c["url"], c["error"]["code"])
        continue
    store.add(                          # your vector store
        id=c["chunkId"],
        text=c["text"],
        metadata={"source": c["url"], "title": c["title"], "pages": c["pages"], "heading": c["heading"]},
    )
```

Because `chunkId` is stable for the same file and settings, re-running a PDF lets you upsert instead of duplicating. Answers can cite `source` and `pages`.

For LangChain, LlamaIndex or Haystack, load the dataset items with the Apify integration of your framework and map `text` to the page content and the other fields to metadata.

### Use with AI agents and the Apify MCP server

The Actor is a normal Apify Actor, so any agent that speaks to the **Apify MCP server** can call it as a tool: add `mzansi-dataworks/pdf-to-markdown-tables-ocr` to the tools of your MCP client (or let it discover Actors by search) and ask for "the text of this PDF" or "the tables on pages 4 to 6". Two tips for agents:

- Use `pageRange` and `outputMode: "pages"` to read only what the question needs; you pay per page.
- Read `status`, `warnings` and `error.code` from the result: a `partial` or `empty` item tells the agent what to change (raise the cost limit, switch OCR on, give a password) instead of silently returning nothing.

### Privacy and safety

- Only `http` and `https` links are fetched, with limits on redirects, time and size. Private, loopback, link-local and cloud-metadata addresses are refused, and checked again after every redirect.
- PDFs are parsed in a separate worker process with no API tokens in its environment. Nothing inside a PDF is ever executed (no JavaScript, no launch actions).
- The password you give is a secret input; it is never written to the log or to the dataset.
- The Actor outputs what is in the PDFs you give it. If those contain personal data, you are responsible for how you use the output.

# Actor input Schema

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

Links to the PDF files (http or https). Paste one link, or several, or upload a text file with one link per line. The link must lead straight to the file, not to a web page that contains it. Public pages only: private addresses and local networks are refused.

## `outputMode` (type: `string`):

Document: one dataset item per PDF with the whole Markdown and all tables. Pages: one item per page. Chunks: ready-made pieces of text for a vector database, each with its page numbers and the heading it sits under.

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

Only read these pages, for example 1-10,15 or 20- (page 20 to the end) or -5 (the first five). Page 1 is the first page of the file. Leave empty for all pages. You are charged only for the pages that are read.

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

Detect tables and return them as Markdown tables inside the text and as separate JSON (rows, header, row objects). Works best on tables with ruled lines or clear column alignment. Switch off to get plain running text only.

## `ocr` (type: `string`):

Auto: OCR only the pages that have no usable text (scans, photos of documents), and read every other page from its text layer. Off: never OCR. Force: OCR every page. OCR reads printed English and Chinese text and digits. Accented letters are often returned without accents. It does not read handwriting or right-to-left, Cyrillic, Greek or Indic scripts. OCR pages cost more than text pages (see Pricing).

## `includeMarkdown` (type: `boolean`):

Return the text as Markdown (headings, lists, GitHub tables). In chunk mode the chunks are Markdown when this is on.

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

Also return the same content as plain text without Markdown symbols. In chunk mode the chunks are plain text when Markdown is off.

## `includePageMarkers` (type: `boolean`):

In one-item-per-PDF mode, add a marker before the first text of each page (an HTML comment in Markdown: ).

## `removeHeadersFooters` (type: `boolean`):

Drop lines that repeat at the top or bottom of most pages (document title, confidentiality notice, page numbers).

## `chunkSize` (type: `integer`):

Target maximum length of one chunk in characters, not tokens. About 4 characters make one English token, so 1500 characters is roughly 375 tokens. Used in chunk mode.

## `chunkOverlap` (type: `integer`):

How much text from the end of one chunk is repeated at the start of the next. Must not be more than half the chunk size. Used in chunk mode.

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

Password for encrypted PDFs. The same password is tried on every PDF in the run. Stored as a secret and never written to the log or the dataset.

## `ocrMinChars` (type: `integer`):

In Auto mode a page is sent to OCR when its text layer has fewer readable characters than this, or when the text layer is garbled (broken font encoding).

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

Longer PDFs are cut after this many pages and marked as partial. Use a page range to read the rest.

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

Larger files are refused before they are fully downloaded and are not charged.

## Actor input object example

```json
{
  "pdfUrls": [
    {
      "url": "https://mozilla.github.io/pdf.js/web/compressed.tracemonkey-pldi-09.pdf"
    }
  ],
  "outputMode": "document",
  "extractTables": true,
  "ocr": "auto",
  "includeMarkdown": true,
  "includeText": false,
  "includePageMarkers": true,
  "removeHeadersFooters": true,
  "chunkSize": 1500,
  "chunkOverlap": 200,
  "ocrMinChars": 25,
  "maxPagesPerDocument": 500,
  "maxFileSizeMb": 50
}
```

# Actor output Schema

## `results` (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://mozilla.github.io/pdf.js/web/compressed.tracemonkey-pldi-09.pdf"
        }
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("mzansi-dataworks/pdf-to-markdown-tables-ocr").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://mozilla.github.io/pdf.js/web/compressed.tracemonkey-pldi-09.pdf" }] }

# Run the Actor and wait for it to finish
run = client.actor("mzansi-dataworks/pdf-to-markdown-tables-ocr").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://mozilla.github.io/pdf.js/web/compressed.tracemonkey-pldi-09.pdf"
    }
  ]
}' |
apify call mzansi-dataworks/pdf-to-markdown-tables-ocr --silent --output-dataset

```

## MCP server setup

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

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/yW1aOQcez8Ltrpr9z/builds/sTgXXjiaKKppjXqtN/openapi.json
