PDF to Markdown (pdf2md)
Turn PDFs into Markdown, find which pages are scans, pull out images. Nothing is stored.
Documentation
PDF to Markdown. Version 0.3.2.
This API needs an account. Sign in or create one, make an API key for pdf2md there, and send it as X-Api-Key: ak_… (or an access token as Authorization: Bearer). Every account starts with a free allowance of conversions; GET /api/v1/usage says what is left, and so do the X-Usage-* headers on each answer — X-Usage-Reset among them on a monthly plan, the moment its counter starts over. A used-up plan answers 402 with where to upgrade.
Everything is parsed in process by a WebAssembly module. Uploads are held in memory for the length of the request and are never written to disk or sent anywhere else.
Convert
curl -X POST https://pdf2md-api.dudko.dev/api/v1/convert \
-F [email protected] \
-F profile=compact
Returns text/markdown. Add ?format=json for the full result — page count, detected type, layout, which pages would need OCR.
With the pictures: ?format=zip answers application/zip holding <name>.md and assets/img-1.png, assets/img-2.jpg, … exactly as the Markdown links them. ?format=json&images=true carries the same files base64 in assets. JPEGs pass through as they are; everything else — Flate and LZW samples, CCITT fax, JBIG2, JPEG 2000 — is written as PNG (jpx=keep keeps a JPEG 2000 as the .jp2 it is). Inline images and the pictures in annotations — stamps, signatures, form fields — have no place in the text, so they are linked at the end of their page. So are the vector figures: charts, diagrams and logos drawn with path operators come out as SVG, with the page's text inside them as <text> (vectors=false leaves them out). skipped lists what could not be turned into a file and why, and the Markdown says so where the picture was. X-Pdf-Images counts the files.
The body may also be a raw application/pdf, or JSON {"pdf": "<base64>", "options": {…}}. Options work as query parameters in every case.
Try it
From this page, against this instance. The file goes to the endpoint below and nowhere else; nothing is kept after the response.
Detect
curl -X POST https://pdf2md-api.dudko.dev/api/v1/detect --data-binary @report.pdf
Classification only, in milliseconds: is this text, a scan or a mix, how many pages, and which of them would need OCR. Use it to route work before paying for a conversion.
Plain text
curl -X POST https://pdf2md-api.dudko.dev/api/v1/text --data-binary @report.pdf
Options
| Option | Meaning |
|---|---|
pages | 1-indexed selection: 1,3,5-7 |
password | for an encrypted document |
profile | fidelity (default) or compact, which spends fewer tokens |
includePageMarkers | insert <!-- Page N --> between pages |
includeImages, includeLinks | emit image placeholders / extracted hyperlinks |
stripHeadersFooters | drop running headers and footers |
detectHeaders, detectLists, detectCode, detectBold, detectItalic | turn individual detectors off with =false |
detectUnderline | =true wraps text with a line under it in <u>; off by default, because rules and form lines pass for underlining far more often than underlining occurs |
removePageNumbers, formatUrls, fixHyphenation | post-processing, all on by default |
baseFontSize | body text size in points, when the heuristic gets it wrong |
textOnImagePages | on by default: a page the detector calls a scan still gives up the text it carries (a caption under a full-page figure); =false to get no Markdown for such documents |
GET /api/v1/options returns the same list as JSON, plus this instance's limits.
For an assistant
An MCP endpoint at https://pdf2md-api.dudko.dev/mcp is the front door to this API for an assistant: api_access says how the API works, and api_request checks the options and returns the exact request with a short-lived credential for the signed-in account. No document passes through it. Point an MCP client at that address; it will find the rest — which authorization server issues the tokens, and what scope to ask for — at https://pdf2md-api.dudko.dev/.well-known/oauth-protected-resource/mcp. A token is accepted only if it was issued for this address and no other.
Response headers
| Header | Meaning |
|---|---|
X-Pdf-Type | TextBased, Scanned, ImageBased or Mixed |
X-Pdf-Pages | page count |
X-Pdf-Images | image files in the answer, when images were asked for |
X-Pdf-Needs-Ocr | comma-separated 1-indexed pages, empty when none |
X-Pdf-Encoding-Issues | true when the text is probably garbled |
X-Pdf-Ms | conversion time |
Status codes
401 | missing or wrong API key, or the wrong password for an encrypted PDF |
|---|---|
413 | larger than 32 MB, or more than 300 pages |
415 | not a PDF |
422 | a PDF, but nothing could be extracted — a scan, usually |
429 | rate limit or daily quota |
503 | the queue is full; retry shortly |
504 | the conversion hit the deadline |
This is not OCR
A scanned document is reported as Scanned with no Markdown and a 422 from /convert. Run it through an OCR tool first. Watch X-Pdf-Encoding-Issues as well: text came out, but the fonts declare an encoding the bytes do not honour, so the characters are probably wrong.
Doing it yourself
The same conversion runs in a browser and in Node with no server at all — npm i @dudko.dev/pdf-to-md-core, or npx @dudko.dev/pdf-to-md-cli file.pdf. The web app at pdf2md.dudko.dev never calls this API: it runs the same module in the visitor's browser.