# Thai PDF to Markdown (`vetsuyan-labs/thai-pdf-to-markdown`) Actor

Convert Thai PDFs into clean, LLM-ready Markdown for RAG and AI agents. Copies the exact text from the PDF, catches Thai PDFs whose text is secretly broken, and uses fine-tuned Thai OCR only where needed. Private, pay per page.

- **URL**: https://apify.com/vetsuyan-labs/thai-pdf-to-markdown.md
- **Developed by:** [Parin Pipake](https://apify.com/vetsuyan-labs) (community)
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $3.00 / 1,000 page from the pdf's text layers

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

**Thai PDF to Markdown with the PDF's exact text. Thai PDFs whose text is secretly broken are caught and
re-read. Ready for RAG and AI agents.**

แปลง PDF ภาษาไทยเป็น Markdown ที่สะอาด โดยใช้ข้อความจริงจาก PDF พร้อม OCR ภาษาไทย สำหรับงาน RAG และ AI

- **The PDF's own text where it exists.** Many Thai PDFs already contain their text. It is copied from the
  PDF, with no AI rewriting, in about 2 seconds per page. Headings, tables and reading order are rebuilt
  from the page layout.
- **Broken text is caught.** Many Thai PDFs look right on screen but hold the wrong text: legacy Thai fonts,
  wrong encodings, and the TH SarabunPSK bug where "การ" comes out as "กำร". A page that shows these faults
  is read by OCR instead. Not every broken page is caught: see [Good to know](#good-to-know).
- **Thai OCR only where it's needed.** Scanned pages are read with a Thai OCR model fine-tuned on Thai
  government documents. See [Accuracy](#accuracy).
- **Private.** Documents are processed in memory, never stored or viewed by us, and never used for training.
- **Built for pipelines.** PDF links in, Markdown out: API, Python, n8n, Make, Zapier or AI agents (Apify MCP
  server).

**See it on real pages:** [four Thai government PDF pages, before and after](https://vetsuyan-labs.pages.dev/),
each shown as printed, as stored inside the PDF, and as this Actor returns it.

### Quick start

Paste PDF links into **PDF URLs**, click **Start**, then open the **Output** tab. The form starts at 5 pages
per PDF so a first test costs almost nothing; clear **Max pages per PDF** to convert whole files.

| Field | Type | Default | Notes |
|---|---|---|---|
| `urls` | array of strings | — | HTTPS links to PDF files. Up to 100 links per run; the same file linked twice is converted once. |
| `maxPages` | integer | 1000 | Optional. Each PDF is converted whole, up to 1,000 pages; set a lower number to convert only the first N pages. |

One request, for a few pages (a run longer than 300 seconds returns HTTP 408):

```bash
curl -X POST "https://api.apify.com/v2/acts/vetsuyan-labs~thai-pdf-to-markdown/run-sync-get-dataset-items" \
  -H "Authorization: Bearer $APIFY_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"urls": ["https://example.com/report.pdf"], "maxPages": 5}'
```

Python, for whole documents, with [apify-client](https://pypi.org/project/apify-client/) 3 or newer:

```python
from datetime import timedelta

from apify_client import ApifyClient

client = ApifyClient("<APIFY_TOKEN>")
run = client.actor("vetsuyan-labs/thai-pdf-to-markdown").call(
    run_input={"urls": ["https://example.com/report.pdf"]},
    run_timeout=timedelta(hours=1),  # 200 scanned pages take about 17 minutes
)

## One item per page
for page in client.dataset(run.default_dataset_id).iterate_items():
    print(page["page"], page["source"], page["error"] or page["markdown"][:80])

## Each document as one Markdown file
store = client.key_value_store(run.default_key_value_store_id)
for doc in store.get_record("OUTPUT")["value"]:
    if doc["markdownKey"]:
        markdown = store.get_record(doc["markdownKey"])["value"]
    if doc["error"] or doc["pagesSkipped"] or doc["pagesFailed"]:
        print(doc["sourceUrl"], "is incomplete:", doc["error"] or "pages are missing")
```

### Output

One dataset item per page:

```json
{
  "sourceUrl": "https://example.com/report.pdf",
  "page": 3,
  "source": "text_layer",
  "markdown": "## ตารางที่ 1 ...",
  "avgConfidence": null,
  "processingMs": 1650,
  "error": null
}
```

- `source` is `text_layer` (copied from the PDF) or `ocr`. `avgConfidence` is set for OCR pages only.
- A blank page has empty `markdown`. A page that cannot be converted has an `error`, and the rest of the
  document continues. A file that cannot be processed (not a PDF, too large, password-protected, download
  blocked) is one item with `page: null` and an `error`.

In the key-value store:

- `document-1.md`, `document-2.md`, … hold each file's full Markdown, in the order of your links
  (`markdownKey` in `OUTPUT` names each). Pages are separated by `<!-- page N -->` comments.
- `OUTPUT` lists, for each link the run reached: `sourceUrl`, `markdownKey`, `pagesConverted`, `pagesTotal`,
  `pagesSkipped`, `pagesFailed`, `error` and `stoppedByLimit`.
- **Check `pagesSkipped` and `pagesFailed`.** If either is above 0, the Markdown is not the whole file.
  Skipped pages were beyond 1,000 pages, your `maxPages` or your spending limit (`stoppedByLimit`). A failed
  page leaves a `<!-- page N: could not be converted -->` comment in its place.

### Accuracy

**Digital PDFs.** On pages reported as `text_layer`, the text is the PDF's own. No AI model reads or rewrites
it, so no word is invented. It is only tidied: spacing, line wrapping, the order and encoding of Thai vowel
and tone marks, invisible characters, and letters a PDF draws twice. What can still go wrong is the
structure: a heading that is not marked as one, a table that comes out as plain text, or a line taken for a
page header or footer and left out.

**Scanned pages** are read by OCR, and OCR makes mistakes. We ran this Actor on
[ThaiOCRBench](https://huggingface.co/datasets/typhoon-ai/ThaiOCRBench), a public benchmark of real Thai
images. Scores run from 0 to 1; higher is better.

| What was tested | This Actor | Gemini 2.5 Pro | Claude Sonnet 4 | GPT-4o | Qwen2.5-VL 72B |
|---|---|---|---|---|---|
| Reading a full printed page (197 images) | 0.786 | 0.897 | 0.661 | 0.610 | 0.720 |
| Reading a table (193 images) | 0.444 | 0.686 | 0.650 | 0.571 | 0.549 |
| Reading handwriting (209 images) | 0.156 | 0.714 | 0.301 | 0.489 | 0.393 |

- **Printed pages:** a good fit. Expect some wrong characters on every scanned page, more on old or faint ones.
- **Tables in scans:** simple tables work, but the large models do better. Check any figures you rely on.
- **Handwriting:** not what this Actor is for.

The other models' scores were published by the benchmark's authors in 2025
([paper](https://arxiv.org/abs/2511.04479)); we did not test those models, and their newer versions may do
better. Ours were measured in October 2026. Some table images were used while we developed this Actor; on the
129 that were not, it scores 0.414.

### Pricing

Pay per converted page. No subscription.

| Event | Price | Per 1,000 pages |
|---|---|---|
| Page from the PDF's text layer | $0.003 | $3 |
| Page that needed Thai OCR | $0.010 | $10 |

- A page counts as OCR when it was read whole by OCR (a scanned page, a page whose own text is broken, or a
  page with very little text beside a picture, such as many cover pages), or when OCR read 20 or more letters
  and digits in the pictures or charts of a digital page. Many digital documents contain a few OCR pages.
- Example: a 100-page report with 70 digital and 30 scanned pages costs **$0.51**.
- **Blank pages, failed pages and failed documents are free.** Apify's start fee for a run (about $0.0002)
  may also apply.
- Apify's free plan ($5 a month) covers about 1,600 text pages or 500 OCR pages.
- Set a maximum cost per run in Apify and the Actor stops cleanly when it is reached. A new run starts every
  PDF from page 1 and charges those pages again, so set the limit to cover the whole job.

### Privacy and security

- **We don't keep or look at your documents.** They are processed in memory and never written to disk.
  Results (your links, the converted text and a progress record) go only to your own Apify storage and are
  deleted according to your plan's data retention, or at once when you delete the run.
- **Never used for training.** Your documents and results do not train or improve any model.
- **A run you share with us** (Apify's "Share run data with developers" setting, or a run attached to an
  issue report) is used only to answer your request. We keep no copies.
- **Limited permissions, private logs.** The Actor uses only this run's storage. Logs show each link's host
  name, page counts and error types: never the path, the query string or any document text.
- **Safe downloads.** HTTPS only, with certificates verified. Private and internal network addresses are
  refused.
- **For sensitive documents,** pass short-lived signed links that outlive the run timeout you set.
- **Treat the output as untrusted content.** The Markdown is the document's own text. It can contain text
  written to manipulate AI models (prompt injection), HTML or links: apply your usual safeguards before
  passing it to an AI agent or showing it as a web page.

**Personal data (Thailand PDPA).** If your documents contain personal data, you are the data controller and
we are your data processor: we process documents only to convert them as you request, keep them only in
memory while the run lasts, and never share them. Processing runs on Apify's platform, hosted outside
Thailand, so it involves a transfer abroad: you are responsible for having a basis for it (PDPA sections
28–29), and Apify's [Data Processing Addendum](https://docs.apify.com/legal/data-processing-addendum) covers
Apify's part as our sub-processor. If we become aware of a security incident affecting this Actor we will
publish a notice on this page and notify Apify without undue delay. For data requests or privacy questions,
email the contact address on our Apify profile.

### Good to know

- **Headers, footers and seals are removed:** page numbers, running headers and official stamps. Now and then
  the first or last line of a page is taken for a header or footer and left out, and on a scan, text touching
  a stamp can go with it.
- **Scans:** old or low-quality scans contain more wrong characters. A line that cannot be read reliably is
  left out rather than guessed.
- **Scanned PDFs that are already searchable:** if you can select the text in a scanned PDF, another program
  has already added its own reading of each page. This Actor normally uses that text (`source` is then
  `text_layer`, at the lower price), so any mistakes in it come through. Check a few pages first.
- **The Thai font bug, the other way round:** a PDF where "สำ" is exported as "สา" ("สาหรับ", "อาเภอ") usually
  passes through as written.
- **Tables:** tables with ruling lines become Markdown tables. On digital pages, a layout that cannot be read
  reliably as a table is output as plain text in reading order, not as a wrong table.
- **Pictures, charts and forms:** text inside pictures and charts is read by OCR and may come out fragmented;
  a label shorter than 20 letters and digits is left out. Values typed into PDF form fields are included.
- **Files the Actor cannot fetch:** some sites block automated downloads. For those and for local files,
  pass a link you control that works without logging in, such as a pre-signed S3 or Google Cloud Storage
  URL.
- **Limits:** 100 MB per file, 5 minutes per download, at most 3 redirects, and standard HTTPS links only. Of
  a file longer than 1,000 pages the first 1,000 are converted; if you need more, tell us in the **Issues**
  tab.
- **Large jobs:** a page takes about 2 seconds from the PDF's own text and about 4–6 seconds with OCR, so
  1,000 scanned pages cost $10 and can take an hour and a half. Set the run timeout to match. If a run times
  out, **resurrect** it with a longer timeout: it continues where it stopped and never charges a page twice.

### ภาษาไทย

แปลงไฟล์ PDF ภาษาไทย (รายงาน ระเบียบ ราชกิจจานุเบกษา คู่มือ เอกสารสแกน) เป็น Markdown ที่สะอาด
พร้อมใช้กับ RAG และ AI

- **แม่นยำเมื่อ PDF มีข้อความอยู่แล้ว:** คัดลอกข้อความจริงจาก PDF โดยไม่ใช้ AI เขียนใหม่ พร้อมหัวข้อ ตาราง
  และลำดับการอ่าน
- **เข้าใจปัญหา PDF ภาษาไทย:** ตรวจจับฟอนต์ไทยที่เสียโดยอัตโนมัติ เช่น "การ" ที่กลายเป็น "กำร"
  แล้วอ่านหน้านั้นด้วย OCR แทน
- **OCR ภาษาไทยเฉพาะหน้าที่จำเป็น:** หน้าที่เป็นภาพสแกนจะอ่านด้วยโมเดล OCR ภาษาไทยที่ปรับแต่งจากเอกสารราชการจริง
- **เป็นส่วนตัว:** ประมวลผลในหน่วยความจำ เราไม่เก็บหรือเปิดดูเอกสารของคุณ ไม่นำไปฝึกโมเดล
  และผลลัพธ์อยู่ในพื้นที่จัดเก็บ Apify ของคุณเองเท่านั้น
- **ราคา:** จ่ายตามจำนวนหน้า หน้าว่างและไฟล์ที่ผิดพลาดไม่คิดเงิน
- **ดูตัวอย่างจริง:** [เอกสารราชการ 4 หน้า ก่อนและหลังแปลง](https://vetsuyan-labs.pages.dev/)

# Actor input Schema

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

HTTPS links to PDF files (up to 100 MB each, at most 100 links per run). For local files, upload them first and pass a link that works without logging in, such as a pre-signed S3 URL or a signed Apify key-value store record link.

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

Each PDF is converted whole, up to 1000 pages. Set a lower number to convert only the first pages of each PDF. The form starts at 5 for a cheap first test: clear the field to convert whole files.

## Actor input object example

```json
{
  "urls": [
    "https://upload.wikimedia.org/wikipedia/commons/5/59/%E0%B8%9E%E0%B8%A3%E0%B8%B0%E0%B8%A3%E0%B8%B2%E0%B8%8A%E0%B8%9A%E0%B8%B1%E0%B8%8D%E0%B8%8D%E0%B8%B1%E0%B8%95%E0%B8%B4%E0%B9%82%E0%B8%A3%E0%B8%84%E0%B8%95%E0%B8%B4%E0%B8%94%E0%B8%95%E0%B9%88%E0%B8%AD_%E0%B8%9E.%E0%B8%A8._%E0%B9%92%E0%B9%95%E0%B9%95%E0%B9%98.pdf"
  ],
  "maxPages": 5
}
```

# Actor output Schema

## `pages` (type: `string`):

One item per converted page (or one error item per PDF that could not be processed).

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

document-1.md, document-2.md, ...: the whole Markdown of each PDF, in input order.

## `summary` (type: `string`):

OUTPUT: for each PDF, its Markdown key, pages converted and total, and any error.

# 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://upload.wikimedia.org/wikipedia/commons/5/59/%E0%B8%9E%E0%B8%A3%E0%B8%B0%E0%B8%A3%E0%B8%B2%E0%B8%8A%E0%B8%9A%E0%B8%B1%E0%B8%8D%E0%B8%8D%E0%B8%B1%E0%B8%95%E0%B8%B4%E0%B9%82%E0%B8%A3%E0%B8%84%E0%B8%95%E0%B8%B4%E0%B8%94%E0%B8%95%E0%B9%88%E0%B8%AD_%E0%B8%9E.%E0%B8%A8._%E0%B9%92%E0%B9%95%E0%B9%95%E0%B9%98.pdf"
    ],
    "maxPages": 5
};

// Run the Actor and wait for it to finish
const run = await client.actor("vetsuyan-labs/thai-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 = {
    "urls": ["https://upload.wikimedia.org/wikipedia/commons/5/59/%E0%B8%9E%E0%B8%A3%E0%B8%B0%E0%B8%A3%E0%B8%B2%E0%B8%8A%E0%B8%9A%E0%B8%B1%E0%B8%8D%E0%B8%8D%E0%B8%B1%E0%B8%95%E0%B8%B4%E0%B9%82%E0%B8%A3%E0%B8%84%E0%B8%95%E0%B8%B4%E0%B8%94%E0%B8%95%E0%B9%88%E0%B8%AD_%E0%B8%9E.%E0%B8%A8._%E0%B9%92%E0%B9%95%E0%B9%95%E0%B9%98.pdf"],
    "maxPages": 5,
}

# Run the Actor and wait for it to finish
run = client.actor("vetsuyan-labs/thai-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 '{
  "urls": [
    "https://upload.wikimedia.org/wikipedia/commons/5/59/%E0%B8%9E%E0%B8%A3%E0%B8%B0%E0%B8%A3%E0%B8%B2%E0%B8%8A%E0%B8%9A%E0%B8%B1%E0%B8%8D%E0%B8%8D%E0%B8%B1%E0%B8%95%E0%B8%B4%E0%B9%82%E0%B8%A3%E0%B8%84%E0%B8%95%E0%B8%B4%E0%B8%94%E0%B8%95%E0%B9%88%E0%B8%AD_%E0%B8%9E.%E0%B8%A8._%E0%B9%92%E0%B9%95%E0%B9%95%E0%B9%98.pdf"
  ],
  "maxPages": 5
}' |
apify call vetsuyan-labs/thai-pdf-to-markdown --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,vetsuyan-labs/thai-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/loeGNfy29M2bujhJJ/builds/gJdd8fZrNSPKKC8Ox/openapi.json
