SFOLN

Catalogue / Documents & media

PDF Text Extractor – PDF to Text & Markdown

sfoln/pdf-text-extractor · AI, Developer tools, Automation On request

Extract the text of any PDF (URL, upload or key-value record) as Markdown and plain text, page by page, with metadata, outline and scanned-page detection. No OCR, no start fee: $3 per 1,000 PDFs up to 20 pages, $0.10 per 1,000 extra pages.

About

What it does

PDF Text Extractor turns any PDF into clean Markdown and plain text, page by page, straight from the PDF's own text layer: headings, lists and simple tables are rebuilt from the layout, and you also get the document metadata (title, author, producer, dates), the bookmarks and a per-page verdict on which pages are scans without text. Give it PDF URLs, an uploaded file, data: URIs or Apify key-value store records; each PDF becomes one dataset record. Built for RAG pipelines, search indexing, LLM prompts and AI agents that need the text of born-digital PDFs fast and cheap.

Price

Price per delivered record

EventPricePer 1,000Charged for
PDF document
pdf
$0.003$3 / 1,000One PDF read from its text layer: text, Markdown, metadata and per-page details for up to 20 pages.
Extra PDF page
pdf-page
$0.0001$0.10 / 1,000Each page returned beyond the first 20 of a PDF.

Apify free plan: up to 10 items per run. Paid plans have no per-run cap; set maxTotalChargeUsd to cap spend.

Run

Call it from code, an agent or the Console

This API is private on Apify for now. Email us and we will publish it for you, usually within two working days. The calls below work once it is public. contact@sfoln.com

curl · synchronous run, rows in the response
curl -X POST "https://api.apify.com/v2/actors/sfoln~pdf-text-extractor/run-sync-get-dataset-items" \
  -H "Authorization: Bearer $APIFY_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"pdfUrls":["https://www.w3.org/WAI/ER/tests/xhtml/testfiles/resources/pdf/dummy.pdf"]}'
mcp.json · hosted Apify MCP server
{
  "mcpServers": {
    "apify": {
      "type": "http",
      "url": "https://mcp.apify.com?tools=sfoln/pdf-text-extractor"
    }
  }
}
asynchronous run · for large batches
POST https://api.apify.com/v2/actors/sfoln~pdf-text-extractor/runs?maxTotalChargeUsd=1
GET  https://api.apify.com/v2/actor-runs/{runId}            # until status is SUCCEEDED
GET  https://api.apify.com/v2/datasets/{defaultDatasetId}/items?format=json

Authenticate with an Apify API token. MCP clients can instead sign in with OAuth when prompted.

Input

Input fields

The PDFs to read: URLs, an uploaded file, data: URIs or key-value store records. Each PDF produces one dataset record with its text, Markdown, metadata and per-page details.

FieldTypeDefaultMeaning
pdfUrlsarray—PDF URLs
PDFs to extract, one per line: public http(s) URLs, data: URIs (base64) or Apify key-value store records (kvs://<storeId>/<key>, or the record's API URL, read with the run's token). Max 50 MB per PDF; the file type is detected from its bytes, not its name. Accounts on the Apify free plan can process up to 10 PDFs per run (the first 20 pages of each).
pdfFilestring—Or upload a PDF
A PDF from your computer (or its URL). It is processed together with the PDF URLs above.
pagesstring—Pages
Page range to extract, e.g. "1-5, 8, 10-" ("10-" = from page 10 to the end, "-3" = the first three pages). Empty = every page.
outputFormatstring"both"Output format
Markdown (headings, lists and tables rebuilt from the layout), plain text, or both. One of "both", "markdown", "text".
includePagesbooleantruePer-page output
Return a pages[] list with each page's text, Markdown and text-layer verdict (besides the whole document's text).
includeOutlinebooleanfalseInclude bookmarks (outline)
Return the PDF's bookmarks (table of contents) with the page each one points to.
passwordstring—PDF password
Password of encrypted PDFs (used for every PDF in this run). It is never written to the output.
maxPagesinteger1000Max pages per PDF
Read at most this many pages of each PDF (after the page range). Pages beyond 20 are charged per page. Range 1–5000.
maxItemsinteger1000Max PDFs
Safety cap on how many PDFs are processed in this run; further ones are ignored and counted as inputItemsDropped in the OUTPUT record. Range 1–10000.
concurrencyinteger2Concurrency
PDFs processed in parallel (1–5). Range 1–5.
includeRawbooleanfalseInclude raw metadata
Attach the PDF's raw Info dictionary and XMP properties to each record (raw).
Example input
{
  "pdfUrls": [
    "https://www.w3.org/WAI/ER/tests/xhtml/testfiles/resources/pdf/dummy.pdf"
  ]
}

Output

One record per input item

Records land in the run's dataset; a synchronous call returns them directly. Each has a status (ok is charged; everything else is free) and a charged flag. The run's OUTPUT record totals items and charges.

FieldTypeMeaning
inputstring | objectInput
The PDF reference as submitted (data: URIs are shown as a short label).
statusstringStatus
ok | not_found | error | skipped_invalid | skipped_budget | skipped_free_plan | skipped_unmetered e.g. "ok"
chargedbooleanCharged
Whether the record's pdf event was charged.
chargedEventstringCharged event
Pay-per-event event charged for this record (present only when charged).
chargedCountintegerEvents charged
Events actually charged when the charge came back short (present only then).
sourcestringSource
pdf-text-layer: the PDF's own text layer, read in the Actor.
fetchedAtstringFetched at
ISO timestamp.
errorobjectError
{ code, message, status? } when status is not ok. Codes: invalid_reference, document_too_large, document_not_found, not_a_pdf, not_readable, encrypted_pdf, wrong_password, corrupt_pdf, no_pages, timeout, fetch errors (blocked_url, dns_not_found, network, http_error, …) and the runtime codes (budget_exhausted, free_plan_limit, …).
warningsarrayWarnings
Notes: page limits, budget limits, text moved to the key-value store.
urlstring | nullURL
URL the PDF was downloaded from (after redirects); the record URL for key-value store records; null for data: URIs.
fileNamestring | nullFile name
fileSizeBytesinteger | nullFile size (bytes)
sha256string | nullSHA-256
Hash of the PDF bytes (deduplication).
titlestring | nullTitle
Document title from the metadata, else the first heading.
pageCountinteger | nullPages in PDF
pagesExtractedinteger | nullPages extracted
pageRangestring | nullPage range
The requested page range, or null for every page.
possiblyTruncatedboolean | nullTruncated
Fewer pages were returned than the range selects (maxPagesPerPdf, free plan, budget or time limit).
metadataobject | nullMetadata
{ title, author, subject, keywords[], creator, producer, createdAt, modifiedAt, pdfVersion, language, pageCount, encrypted, linearized, hasForm, pageWidth, pageHeight }
languagestring | nullLanguage
Detected language of the text (ISO 639-1).
hasTextLayerboolean | nullHas text layer
At least one returned page has extractable text.
textPagesinteger | nullPages with text
Pages with a usable text layer.
scannedPagesarray | nullScanned pages
Pages without a usable text layer that show an image or drawings: OCR them with pdf-ocr-markdown.
blankPagesarray | nullBlank pages
ocrRecommendedboolean | nullOCR recommended
ocrSuggestionstring | nullOCR suggestion
charCountinteger | nullCharacters
wordCountinteger | nullWords
markdownstring | nullMarkdown
All returned pages, separated by <!-- page N --> comments.
markdownUrlstring | nullFull Markdown URL
Set when the text exceeded the dataset item size: the full Markdown is in the run's key-value store; markdown holds the first 1 MB.
textstring | nullPlain text
All returned pages as plain text, pages separated by a form feed (\f).
textUrlstring | nullFull text URL
extraPagesChargedinteger | nullExtra pages charged
pdf-page events charged (pages beyond 20).
pagesarray | nullPages
[{ pageNumber, markdown, text, charCount, wordCount, hasTextLayer, textQuality (good | sparse | garbled | none), needsOcr, blank, imageCount, width, height }]
outlinearray | nullOutline
Bookmarks [{ title, pageNumber, level }] (includeOutline).
rawobject | nullRaw metadata
{ info, xmp } (includeRaw).
Example record
Example record
{
  "input": "https://example.com/reports/q1-2026.pdf",
  "status": "ok",
  "charged": true,
  "chargedEvent": "pdf",
  "source": "pdf-text-layer",
  "fetchedAt": "2026-10-14T10:00:00.000Z",
  "url": "https://example.com/reports/q1-2026.pdf",
  "fileName": "q1-2026.pdf",
  "fileSizeBytes": 3337,
  "sha256": "5d1a0b2f8c3e4d6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b1c2d3e4f5a6b7c8d9e0f",
  "title": "Quarterly Report 2026",
  "pageCount": 3,
  "pagesExtracted": 3,
  "pageRange": null,
  "possiblyTruncated": false,
  "metadata": {
    "title": "Quarterly Report 2026",
    "author": "Example Analyst",
    "subject": "Q1 results",
    "keywords": [
      "report",
      "revenue"
    ],
    "creator": "Writer",
    "producer": "LibreOffice 24.2",
    "createdAt": "2026-04-02T09:30:00.000Z",
    "modifiedAt": "2026-04-02T09:30:00.000Z",
    "pdfVersion": "1.7",
    "language": "en-US",
    "pageCount": 3,
    "encrypted": false,
    "linearized": false,
    "hasForm": false,
    "pageWidth": 595.3,
    "pageHeight": 841.9
  },
  "language": "en",
  "hasTextLayer": true,
  "textPages": 3,
  "scannedPages": [],
  "blankPages": [],
  "ocrRecommended": false,
  "ocrSuggestion": null,
  "charCount": 717,
  "wordCount": 118,
  "markdown": "# Quarterly Report 2026\n\n## 1. Summary\n\nRevenue grew in every region during the first quarter of 2026.\n\n- Revenue grew 12 percent year over year.\n- Gross margin improved to 61 percent.\n\n<!-- page 2 -->\n\n## 2. Results by region\n\n| Region | Q1 revenue | Change |\n| --- | --- | --- |\n| North | 1,240 | +14% |\n| South | 980 | +9% |",
  "markdownUrl": null,
  "text": "Quarterly Report 2026\n\n1. Summary\n\nRevenue grew in every region during the first quarter of 2026.\n\n\f\n\n2. Results by region\n\nRegion\tQ1 revenue\tChange",
  "textUrl": null,
  "extraPagesCharged": 0,
  "pages": [
    {
      "pageNumber": 1,
      "markdown": "# Quarterly Report 2026\n\n## 1. Summary\n\n…",
      "text": "Quarterly Report 2026\n\n1. Summary\n\n…",
      "charCount": 303,
      "wordCount": 50,
      "hasTextLayer": true,
      "textQuality": "good",
      "needsOcr": false,
      "blank": false,
      "imageCount": 0,
      "width": 595.3,
      "height": 841.9
    }
  ]
}

FAQ

Questions

Does it read scanned PDFs?
It reads the PDF's own text layer, which scans do not have. Pages without a usable text layer are listed in scannedPages and ocrSuggestion; run PDF OCR to Markdown (pdf-ocr-markdown) on such PDFs, which OCRs only those pages.
How are pages counted and charged?
Each PDF costs one pdf event, which covers up to 20 pages. Every further page costs one pdf-page event ($0.0001). Use the pages range or maxPages to read only the part you need.
Can it open password-protected PDFs?
Yes, when you set the password input. Without it (or with a wrong one) the record has status error with code encrypted_pdf or wrong_password and is not charged.

Machine-readable

This page for agents