API Documentation

One conversion endpoint for demo, trial, and paid usage.

Quickstart

Run this Bash script with curl and Python 3. It validates success and prints Markdown. No signup.

#!/usr/bin/env bash
set -euo pipefail
key=demo_public_key
identity=demo-example
work=$(mktemp -d)
trap 'rm -rf "$work"' EXIT
result=$work/result
status=$(curl --silent --show-error --max-time 660 -o "$result" -w '%{http_code}' \
  "https://pdftomarkdown.dev/v1/convert" \
  -H "Authorization: Bearer $key" -H "Idempotency-Key: $identity" \
  -H "Content-Type: application/json" \
  --data '{"input":{"pdf_url":"https://pdftomarkdown.dev/samples/invoice.pdf"}}')
if [ "$status" != 200 ]; then echo "HTTP $status; keep the identity for recovery" >&2; exit 1; fi
python3 - "$result" <<'PY'
import json, sys
with open(sys.argv[1]) as response:
    result = json.load(response)
if not (isinstance(result, dict) and result.get("complete") is True
        and isinstance(result.get("markdown"), str)
        and type(result.get("pages")) is int and result["pages"] >= 0
        and isinstance(result.get("request_id"), str) and result["request_id"].strip()):
    sys.exit("Incomplete or invalid conversion response")
sys.stdout.write(result["markdown"])
PY

The demo key demo_public_key works instantly. Multi-page PDFs are accepted, but the Hacker tier only processes page 1 and is limited to 3 req/min.

Have Node installed? The CLI converts local files, URLs, or stdin — no install, no signup:

$ npx pdftomarkdown document.pdf > document.md

Set PDFTOMARKDOWN_API_KEY to use your own key. Run npx pdftomarkdown --help for all options, or read the CLI guide.

Using Claude Code? Install the official plugin with /plugin marketplace add ThiloReintjes/pdftomarkdown-skill and Claude reads PDFs on its own.

Processing time: OCR time varies with page count, layout, and provider capacity. For multi-page documents, set your HTTP client timeout to 11 minutes, or use input.max_pages to bound the selected prefix. Measure elapsed time locally. A timeout or Ctrl-C stops waiting; processing and charging may continue.

Review privacy, security, and data retention before sending sensitive documents.

After your first successful request, use the Python local-file batch recipe or the table extraction workflow. For timeouts, pending work or credit shortfalls, follow recovery guidance with the saved identity. The endpoint reference lists request and response fields.

Language guides

Use plain HTTP from any stack. These focused guides are easier to share with implementation teams.

Tiers

Use the demo for page-one tests. Sign in for trial pages, multi-page conversion, and paid credits.

HackerDeveloper
AuthPublic keyGitHub login
PagesPage 1 only20 once
Rate limit3/min per IPNone
WatermarkYesNo

Tier 1 — Hacker

Public demo key, rate-limited to 3 req/min per IP. If you send a multi-page PDF, only page 1 is processed.

curl

#!/usr/bin/env bash
set -euo pipefail
key=demo_public_key
identity=demo-example
work=$(mktemp -d)
trap 'rm -rf "$work"' EXIT
result=$work/result
status=$(curl --silent --show-error --max-time 660 -o "$result" -w '%{http_code}' \
  "https://pdftomarkdown.dev/v1/convert" \
  -H "Authorization: Bearer $key" -H "Idempotency-Key: $identity" \
  -H "Content-Type: application/json" \
  --data '{"input":{"pdf_url":"https://pdftomarkdown.dev/samples/invoice.pdf"}}')
if [ "$status" != 200 ]; then echo "HTTP $status; keep the identity for recovery" >&2; exit 1; fi
python3 - "$result" <<'PY'
import json, sys
with open(sys.argv[1]) as response:
    result = json.load(response)
if not (isinstance(result, dict) and result.get("complete") is True
        and isinstance(result.get("markdown"), str)
        and type(result.get("pages")) is int and result["pages"] >= 0
        and isinstance(result.get("request_id"), str) and result["request_id"].strip()):
    sys.exit("Incomplete or invalid conversion response")
sys.stdout.write(result["markdown"])
PY
{
  "complete": true,
  "markdown": "# CONTOSO LTD.\n\n# INVOICE\n\nContoso Headquarters\n\n123 456th St\n\nNew York, NY, 10001\n\nINVOICE: INV-100\n\nDATE: 11/15/2019\n\nDUE DATE: 12/15/2019\n\nCUSTOMER NAME: MICROSOFT CORPORATION\n\nCUSTOMER ID: CID-12345\n\nMicrosoft Corp\n\n123 Other St,\n\nRedmond WA, 98052\n\nBILL TO:\n\nMicrosoft Finance\n\n123 Bill St,\n\nRedmond WA, 98052\n\nSHIP TO:\n\nMicrosoft Delivery\n\n123 Ship St,\n\nRedmond WA, 98052\n\nSERVICE ADDRESS:\n\nMicrosoft Services\n\n123 Service St,\n\nRedmond WA, 98052\n\n| SALESPERSON | P.O. NUMBER | REQUISITIONER | SHIPPED VIA | F.O.B. POINT | TERMS |\n| --- | --- | --- | --- | --- | --- |\n|  | PO-3333 |  |  |  |  |\n\n<table><tr><th>QUANTITY</th><th>DESCRIPTION</th><th>UNIT PRICE</th><th>TOTAL</th></tr><tr><td>1</td><td>Test for 23 fields</td><td>1</td><td>$100.00</td></tr><tr><td></td><td></td><td></td><td></td></tr><tr><td colspan=\"3\">SUBTOTAL</td><td>$100.00</td></tr><tr><td colspan=\"3\">SALES TAX</td><td>$10.00</td></tr><tr><td colspan=\"3\">TOTAL</td><td>$110.00</td></tr><tr><td colspan=\"3\">PREVIOUS BALANCE</td><td>$500.00</td></tr><tr><td colspan=\"3\">TOTAL DUE</td><td>$610.00</td></tr></table>\n\nTHANK YOU FOR YOUR BUSINESS!\n\nREMIT TO:\n\nContoso Billing\n\n123 Remit St\n\nNew York, NY, 10001\n\n> Processed by pdfToMarkdown.dev",
  "pages": 1,
  "request_id": "req_example_invoice"
}

Tier 1 responses append a watermark: > Processed by pdfToMarkdown.dev

Successful Hacker-tier responses also include X-PdfToMarkdown-Page-Cap: 1 so you can detect the enforced page cap.

Validated Python workflow

Tier 2 — Developer

Sign in with GitHub for a personal key. New accounts receive 20 trial pages once. One delivered page uses one credit; subscription pages expire at the paid period boundary and independent top-ups do not expire.

curl

Use the account curl workflow with an environment key and a saved conversion identity.

{
  "complete": true,
  "markdown": "# CONTOSO LTD.\n\n# INVOICE\n\nContoso Headquarters\n\n123 456th St\n\nNew York, NY, 10001\n\nINVOICE: INV-100\n\nDATE: 11/15/2019\n\nDUE DATE: 12/15/2019\n\nCUSTOMER NAME: MICROSOFT CORPORATION\n\nCUSTOMER ID: CID-12345\n\nMicrosoft Corp\n\n123 Other St,\n\nRedmond WA, 98052\n\nBILL TO:\n\nMicrosoft Finance\n\n123 Bill St,\n\nRedmond WA, 98052\n\nSHIP TO:\n\nMicrosoft Delivery\n\n123 Ship St,\n\nRedmond WA, 98052\n\nSERVICE ADDRESS:\n\nMicrosoft Services\n\n123 Service St,\n\nRedmond WA, 98052\n\n| SALESPERSON | P.O. NUMBER | REQUISITIONER | SHIPPED VIA | F.O.B. POINT | TERMS |\n| --- | --- | --- | --- | --- | --- |\n|  | PO-3333 |  |  |  |  |\n\n<table><tr><th>QUANTITY</th><th>DESCRIPTION</th><th>UNIT PRICE</th><th>TOTAL</th></tr><tr><td>1</td><td>Test for 23 fields</td><td>1</td><td>$100.00</td></tr><tr><td></td><td></td><td></td><td></td></tr><tr><td colspan=\"3\">SUBTOTAL</td><td>$100.00</td></tr><tr><td colspan=\"3\">SALES TAX</td><td>$10.00</td></tr><tr><td colspan=\"3\">TOTAL</td><td>$110.00</td></tr><tr><td colspan=\"3\">PREVIOUS BALANCE</td><td>$500.00</td></tr><tr><td colspan=\"3\">TOTAL DUE</td><td>$610.00</td></tr></table>\n\nTHANK YOU FOR YOUR BUSINESS!\n\nREMIT TO:\n\nContoso Billing\n\n123 Remit St\n\nNew York, NY, 10001",
  "pages": 1,
  "request_id": "req_example_invoice"
}

Set PDFTOMARKDOWN_API_KEY to your GitHub-login key and save an identity before account submission.

Validated Python workflow

API Reference

POST /v1/convert

Request body · application/json

FieldTypeRequiredDescription
input.pdf_urlstringYes*Public URL of a PDF to fetch and convert
input.pdf_base64stringYes*Base64-encoded PDF bytes
input.include_rawbooleanNoAdd one extra raw field for debugging without changing the standard success fields
input.max_pagesintegerNoSelect the first N pages with a positive safe integer. The original PDF must still contain at most 1,000 pages. Hacker tier always uses 1.

* Provide exactly one of input.pdf_url or input.pdf_base64.

You can also upload raw PDF bytes with Content-Type: application/pdf. Put optional max_pages and include_raw=true|false once in the query string. Every source is limited to 50 MiB.

Headers

AuthorizationBearer <api_key>, or the signed-in browser session with X-CSRF-Token
Content-Typeapplication/json
Idempotency-KeyStrongly recommended for account conversions so callers can replay safely; use 8–128 characters from A–Z a–z 0–9 _ . : - and reuse a key only for the identical request

Signed-in browser flows can create an immutable source with POST /v1/conversion-drafts, inspect it with GET /v1/conversion-drafts/{draft_id}, then convert with only {"input":{"draft_id":"…"}}. A draft is accessible for 30 minutes and accepts no source or option overrides. Completed account results replay for 24 hours with the same key. A 409 means pending work, failed work, or a key conflict; a 410 means the retained result or draft expired.

Response · application/json

FieldTypeDescription
completebooleanAlways true on a successful, complete response
markdownstringConverted markdown text
pagesintegerPages processed
request_idstringUnique ID for debugging

Successful responses always include complete: true, markdown, pages, and request_id. Canonical Markdown is never silently truncated. Set input.include_raw to true only if you also want an extra raw field for debugging.

Simple rectangular tables use escaped GFM pipe-table syntax. Complex tables with row spans, column spans, nested tables, or nested block content remain sanitized raw HTML inside the Markdown so their content and structure are not destroyed.

Hacker-tier successes also return X-PdfToMarkdown-Page-Cap: 1; if the source PDF has multiple pages, only page 1 is converted and pages returns 1.

Mistral OCR 4.1 is the current processor. Timing headers are optional: x-queue-ms, x-processing-ms, and x-provider-latency-ms. Provider latency is provider work only, not total request time, and is 0 on cache hits and retained replays.

Errors

Conversion errors include error, message, and request_id. Retry fields and Retry-After appear only when a retry interval is meaningful. For 409, retry the identical immutable request with the same Idempotency-Key when instructed; use a new key only for a new conversion. Drafts expire after 30 minutes and completed account results after 24 hours.

413 Complete response too large

{
  "error": "response_too_large",
  "message": "The complete OCR result exceeds the response size limit. Set input.max_pages or split the PDF into smaller documents.",
  "docs": "https://pdftomarkdown.dev/docs/",
  "request_id": "req_large123",
  "pages": 25,
  "max_response_bytes": 3500000,
  "response_bytes": 3900000
}

422 Bad input

{
  "error": "unprocessable_document",
  "message": "The file could not be parsed as a PDF. Only PDF documents are supported.",
  "request_id": "req_pdf321"
}

401 Unauthorized

{
  "error": "authentication_required",
  "message": "Authenticate with an Authorization: Bearer <api_key> header, or sign in and send the browser session cookie with X-CSRF-Token.",
  "request_id": "req_auth789",
  "accepted_auth": {
    "bearer": { "header": "Authorization: Bearer <api_key>", "signup_url": "https://pdftomarkdown.dev/auth/github" },
    "browser_session": { "sign_in_url": "https://pdftomarkdown.dev/auth/github", "csrf_header": "X-CSRF-Token" }
  }
}

429 Rate limited

{
  "error": "rate_limited",
  "message": "Free Hacker-tier limit reached (3 requests per minute per IP). Retry shortly, or get a free API key with higher limits at https://pdftomarkdown.dev/auth/github.",
  "request_id": "req_rate123",
  "retry_after_seconds": 42
}

429 Credit shortfall

{
  "error": "quota_exceeded",
  "message": "You do not have enough page credits for this conversion. Reduce input.max_pages or add credits when sales are available. A reset_at field is included only when this allowance actually renews.",
  "request_id": "req_quota456",
  "required_pages": 12,
  "available_pages": 5,
  "missing_pages": 7
}

Reduce input.max_pages or add credits. This response has no retry interval because these credits do not automatically replenish. An applicable legacy allowance may include reset_at and retry guidance.

CodeMeaningWhen
200SuccessPDF converted
400Bad RequestRequest payload rejected upstream
401UnauthorizedMissing or invalid API key
422Unprocessable EntityInvalid source URL, TLS failure, unreachable source PDF, or unreadable PDF
413Content Too Largeinput_too_large at 50 MiB, or response_too_large; only the latter can be reduced with max_pages
429Too Many Requestsrate_limited or quota_exceeded; use retry guidance when present; exhausted one-time credits do not automatically reset
502Bad GatewayUpstream worker unreachable, invalid, or failed while processing
504Gateway TimeoutUpstream worker timed out
500Server ErrorInternal — include request_id when reporting

Ready to start?

Try the demo key or sign in with GitHub for 20 trial pages once for new accounts.