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"])
PYThe 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.mdSet 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.
| Hacker | Developer | |
|---|---|---|
| Auth | Public key | GitHub login |
| Pages | Page 1 only | 20 once |
| Rate limit | 3/min per IP | None |
| Watermark | Yes | No |
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.
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.
API Reference
POST /v1/convertRequest body · application/json
| Field | Type | Required | Description |
|---|---|---|---|
input.pdf_url | string | Yes* | Public URL of a PDF to fetch and convert |
input.pdf_base64 | string | Yes* | Base64-encoded PDF bytes |
input.include_raw | boolean | No | Add one extra raw field for debugging without changing the standard success fields |
input.max_pages | integer | No | Select 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
Authorization | Bearer <api_key>, or the signed-in browser session with X-CSRF-Token |
Content-Type | application/json |
Idempotency-Key | Strongly 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
| Field | Type | Description |
|---|---|---|
complete | boolean | Always true on a successful, complete response |
markdown | string | Converted markdown text |
pages | integer | Pages processed |
request_id | string | Unique 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.
| Code | Meaning | When |
|---|---|---|
200 | Success | PDF converted |
400 | Bad Request | Request payload rejected upstream |
401 | Unauthorized | Missing or invalid API key |
422 | Unprocessable Entity | Invalid source URL, TLS failure, unreachable source PDF, or unreadable PDF |
413 | Content Too Large | input_too_large at 50 MiB, or response_too_large; only the latter can be reduced with max_pages |
429 | Too Many Requests | rate_limited or quota_exceeded; use retry guidance when present; exhausted one-time credits do not automatically reset |
502 | Bad Gateway | Upstream worker unreachable, invalid, or failed while processing |
504 | Gateway Timeout | Upstream worker timed out |
500 | Server Error | Internal — 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.